"""Poelsnepstraat first-floor art studio. Blender 4.5+.
Flat ceiling 2.80 m; brick walls, concrete floor and two concrete structural frames.
Coordinates: x north, y west, z above this floor. Run blender -b -P build_studio.py.
"""
import bpy, math, os, json, random
from pathlib import Path
from mathutils import Vector
ROOT=Path(__file__).resolve().parent.parent
bpy.ops.wm.read_factory_settings(use_empty=True)
scene=bpy.context.scene; random.seed(18)
cols={}
for key,name in [('shell','01_Brick_envelope'),('structure','02_Concrete_frames'),('ceiling','03_Flat_ceiling_2800mm'),('stairs','04_Stairs'),('counter','05_Wood_sink_counter'),('wc','06_Toilet_and_basin'),('art','07_Active_art_studio'),('person','08_Margherita_stylized_artist'),('lights','09_Cameras_and_lighting')]:
    c=bpy.data.collections.new(name);scene.collection.children.link(c);cols[key]=c
current='shell'
def reg(o,name,m=None):
    o.name=name
    for c in list(o.users_collection):c.objects.unlink(o)
    cols[current].objects.link(o)
    if m:o.data.materials.append(m)
    return o
def material(name,c,rough=.6,metal=0):
    m=bpy.data.materials.new(name);m.use_nodes=True;p=m.node_tree.nodes.get('Principled BSDF');p.inputs['Base Color'].default_value=(*c,1);p.inputs['Roughness'].default_value=rough;p.inputs['Metallic'].default_value=metal;m.diffuse_color=(*c,1);return m
def noise_mat(name,a,b,scale,rough,bumpdist):
    m=material(name,a,rough);n=m.node_tree.nodes;l=m.node_tree.links;p=n.get('Principled BSDF')
    tex=n.new('ShaderNodeTexNoise');tex.inputs['Scale'].default_value=scale;tex.inputs['Detail'].default_value=3
    coords=n.new('ShaderNodeTexCoord');l.new(coords.outputs['Object'],tex.inputs['Vector'])
    ramp=n.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].color=(*a,1);ramp.color_ramp.elements[1].color=(*b,1);l.new(tex.outputs['Fac'],ramp.inputs[0]);l.new(ramp.outputs[0],p.inputs['Base Color'])
    bump=n.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.18;bump.inputs['Distance'].default_value=bumpdist;l.new(tex.outputs['Fac'],bump.inputs['Height']);l.new(bump.outputs[0],p.inputs['Normal'])
    return m
concrete=noise_mat('Honed warm grey concrete floor',(.24,.235,.215),(.43,.42,.38),2.5,.62,.001)
frame=noise_mat('Exposed concrete structural frames',(.32,.315,.29),(.54,.52,.47),5,.85,.0015)
ceilingmat=noise_mat('Light concrete flat ceiling',(.49,.48,.44),(.65,.64,.59),2,.88,.001)
oak=noise_mat('Natural oak work surfaces',(.24,.13,.061),(.49,.32,.16),4,.50,.0007)
# Elongated grain.
for n in oak.node_tree.nodes:
    if n.type=='TEX_NOISE':
        v=oak.node_tree.nodes.new('ShaderNodeVectorMath');v.operation='MULTIPLY';v.inputs[1].default_value=(4,95,10)
        co=next(n for n in oak.node_tree.nodes if n.type=='TEX_COORD')
        oak.node_tree.links.new(co.outputs['Generated'],v.inputs[0]);oak.node_tree.links.new(v.outputs[0],n.inputs['Vector'])
white=material('Warm white frames and ceramics',(.79,.77,.72),.48)
steel=material('Brushed stainless sink and taps',(.58,.62,.65),.24,1)
black=material('Charcoal painted steel',(.025,.029,.027),.65)
canvas=material('Warm unpainted linen canvas',(.75,.68,.54),.9)
wine=material('Oxblood red studio work mat',(.18,.025,.025),.83)
paintred=material('Oxide crimson paint',(.25,.018,.012),.55)
green=material('Deep forest green paint',(.009,.073,.055),.61)
blue=material('Grey blue paint',(.11,.16,.17),.62)
clay=material('Stoneware water pots',(.31,.20,.12),.8)
glass=material('Clear window glass',(.85,.92,.97),.03)
glass.node_tree.nodes['Principled BSDF'].inputs['Transmission Weight'].default_value=1
mirror=material('Bathroom mirror',(.92,.94,.97),.015,1)
linen=material('Linen cloth',(.6,.55,.45),.95)
# Real-scale brick pattern: convert world coordinates to the wall plane.
def brickmat(name,across):
    m=material(name,(.36,.15,.073),.88);n=m.node_tree.nodes;l=m.node_tree.links;p=n.get('Principled BSDF')
    tc=n.new('ShaderNodeTexCoord');sep=n.new('ShaderNodeSeparateXYZ');comb=n.new('ShaderNodeCombineXYZ')
    l.new(tc.outputs['Object'],sep.inputs[0]);l.new(sep.outputs[across],comb.inputs['X']);l.new(sep.outputs['Z'],comb.inputs['Y'])
    br=n.new('ShaderNodeTexBrick');br.offset=.5;br.offset_frequency=2;br.inputs['Scale'].default_value=1
    br.inputs['Brick Width'].default_value=.225;br.inputs['Row Height'].default_value=.071;br.inputs['Mortar Size'].default_value=.0045
    br.inputs['Color1'].default_value=(.38,.145,.067,1);br.inputs['Color2'].default_value=(.22,.072,.031,1);br.inputs['Mortar'].default_value=(.29,.255,.20,1)
    l.new(comb.outputs[0],br.inputs['Vector']);l.new(br.outputs['Color'],p.inputs['Base Color'])
    bump=n.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.4;bump.inputs['Distance'].default_value=.005;bump.invert=True;l.new(br.outputs['Fac'],bump.inputs['Height']);l.new(bump.outputs[0],p.inputs['Normal']);return m
brickx=brickmat('Warm red-brown brick along side walls','X');bricky=brickmat('Warm red-brown brick end walls','Y')
def box(name,loc,dims,m,bevel=.004,smooth=False):
    bpy.ops.mesh.primitive_cube_add(size=1,location=loc);o=reg(bpy.context.object,name,m);o.dimensions=dims;bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
    if bevel:
        b=o.modifiers.new('Soft edges','BEVEL');b.width=bevel;b.segments=5 if smooth else 3;o.modifiers.new('Weighted normals','WEIGHTED_NORMAL')
    if smooth:
        for f in o.data.polygons:f.use_smooth=True
    return o
def mesh(name,vs,fs,m):
    data=bpy.data.meshes.new(name);data.from_pydata(vs,[],fs);data.update();o=bpy.data.objects.new(name,data);cols[current].objects.link(o)
    if m:o.data.materials.append(m)
    return o
def extrude(name,pts,z0,z1,m):
    n=len(pts);vs=[(x,y,z) for z in [z0,z1] for x,y in pts];fs=[tuple(reversed(range(n))),tuple(range(n,2*n))]+[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)];return mesh(name,vs,fs,m)
def tube(name,pts,r,m):
    c=bpy.data.curves.new(name,'CURVE');c.dimensions='3D';c.bevel_depth=r;c.bevel_resolution=4;s=c.splines.new('POLY');s.points.add(len(pts)-1)
    for p,co in zip(s.points,pts):p.co=(*co,1)
    o=bpy.data.objects.new(name,c);cols[current].objects.link(o);o.data.materials.append(m);return o
def cyl(name,loc,r,h,m,rot=None):
    bpy.ops.mesh.primitive_cylinder_add(vertices=40,radius=r,depth=h,location=loc);o=reg(bpy.context.object,name,m)
    if rot:o.rotation_euler=rot
    b=o.modifiers.new('Edge softness','BEVEL');b.width=min(.004,h/5);b.segments=3;o.modifiers.new('Normals','WEIGHTED_NORMAL')
    for f in o.data.polygons:f.use_smooth=True
    return o
def sphere(name,loc,dims,m):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=24,ring_count=16,radius=1,location=loc);o=reg(bpy.context.object,name,m);o.scale=dims
    for f in o.data.polygons:f.use_smooth=True
    return o
def limb(name,a,b,r,m):
    delta=Vector(b)-Vector(a);o=cyl(name,(Vector(a)+Vector(b))/2,r,delta.length,m);o.rotation_euler=delta.to_track_quat('Z','Y').to_euler();return o
def cut_out(o,loc,dims):
    c=box('Temporary boolean tool',loc,dims,None,0);bpy.context.view_layer.objects.active=o;b=o.modifiers.new('Opening','BOOLEAN');b.operation='DIFFERENCE';b.object=c;bpy.ops.object.modifier_apply(modifier=b.name);bpy.data.objects.remove(c,do_unlink=True)
X0=-4.71;X1=5.04;Y0=-7.02;Y1=.45;H=2.80
# Floor and ceiling retain the south-west stair footprint.
box('Concrete floor main slab',(.165,(Y0-.57)/2,-.105),(9.75,-.57-Y0,.21),concrete,.002)
box('Concrete floor north of staircase',((X1-1.81)/2,(-.57+Y1)/2,-.105),(X1+1.81,Y1+.57,.21),concrete,.002)
box('West exposed brick wall',(.165,.55,H/2),(9.75,.20,H),brickx,.002)
box('East exposed brick wall',(.165,-7.12,H/2),(9.75,.20,H),brickx,.002)
def endwall(name,xx,windows):
    sill=.57;head=2.27;start=Y0
    for cy,w in sorted(windows):
        lo=cy-w/2;hi=cy+w/2
        if lo>start:box(name+' brick pier',(xx,(lo+start)/2,H/2),(.2,lo-start,H),bricky,.001)
        box(name+' brick below window',(xx,cy,sill/2),(.2,w,sill),bricky,.001)
        box(name+' brick window head',(xx,cy,(head+H)/2),(.2,w,H-head),bricky,.001)
        for yy in [lo+.029,hi-.029]:box(name+' white window jamb',(xx,yy,(sill+head)/2),(.15,.058,head-sill),white,.003)
        for z in [sill+.029,head-.029]:box(name+' white window rail',(xx,cy,z),(.15,w,.058),white,.003)
        box(name+' white central mullion',(xx,cy,(sill+head)/2),(.15,.04,head-sill),white,.002)
        box(name+' clear glazing',(xx,cy,(sill+head)/2),(.008,w-.09,head-sill-.09),glass,.001)
        box(name+' oak window sill',(xx+(.035 if xx<0 else -.035),cy,sill),(.30,w+.025,.028),oak,.002)
        start=hi
    if start<Y1:box(name+' brick end pier',(xx,(start+Y1)/2,H/2),(.2,Y1-start,H),bricky,.001)
endwall('South',X0-.1,[(-1.31,1.40),(-3.34,1.40),(-5.35,1.40)])
endwall('North',X1+.1,[(-1.86,1.48),(-4.84,1.48)])
current='structure'
for x,w in [(-1.625,.37),(1.59,.30)]:
    for y in [Y1-.51,Y0+.51]:box('Concrete dividing pier',(x,y,H/2),(w,1.02,H),frame,.008)
    box('Concrete transverse downstand beam',(x,(Y0+Y1)/2,H-.15),(w,7.47,.30),frame,.008)
# Existing notch in the north-east corner, simplified from the first-floor plan.
box('North-east masonry return',(4.73,-6.72,H/2),(.62,.60,H),bricky,.002)
current='ceiling'
box('Flat concrete ceiling main',(.165,(Y0-.57)/2,H+.105),(9.75,-.57-Y0,.21),ceilingmat,.001)
box('Flat concrete ceiling north of stairs',((X1-1.81)/2,(-.57+Y1)/2,H+.105),(X1+1.81,Y1+.57,.21),ceilingmat,.001)
current='stairs'
# Same winding staircase footprint as the loft, now with the flight rising to it.
def stair_flight(offset):
    pivot=(-4.18,-.04)
    for i in range(3):
        a0=math.radians(-90+i*30);a1=a0+math.pi/6;pts=[pivot,(pivot[0]+.50*math.cos(a0),pivot[1]+.50*math.sin(a0)),(pivot[0]+.50*math.cos(a1),pivot[1]+.50*math.sin(a1))]
        z=offset+.177*(i+1);extrude('Oak lower winder',pts,z-.045,z,oak)
    for i in range(11):
        xx=-4.22+i*.20;z=offset+.177*(i+4);box('Oak 20cm stair tread',(xx,-.05,z-.0225),(.205,.89,.045),oak,.004);box('Concrete stair riser',(xx-.10,-.05,z-.09),(.025,.89,.177),frame,.002)
    pivot=(-1.94,.34)
    for i in range(3):
        a0=math.radians(180+i*30);a1=a0+math.pi/6;pts=[pivot,(pivot[0]+.85*math.cos(a0),pivot[1]+.85*math.sin(a0)),(pivot[0]+.85*math.cos(a1),pivot[1]+.85*math.sin(a1))]
        z=offset+.177*(i+15);extrude('Oak upper winder',pts,z-.045,z,oak)
stair_flight(0);stair_flight(-3.009)
# Studio arrival landing and guard around lower opening.
box('Stair arrival landing',(-2.23,-1.015,-.08),(.88,.89,.16),frame,.004)
for i in range(13):
    xx=-4.58+i*.13;tube('Slim white stair guard',[(xx,-.60,0),(xx,-.60,1.0)],.008,white)
tube('Oak landing handrail',[(-4.60,-.60,1.01),(-2.98,-.60,1.01)],.024,oak)
tube('Ascending stair handrail',[(-4.26,-.57,1.10),(-2.16,-.57,3.06)],.025,oak)
# SIMPLE WOOD COUNTER, sink centered, no cooking appliances.
current='counter'
top=box('Middle bay solid oak sink counter',(0,0,.90),(2.85,.90,.045),oak,.006)
cut_out(top,(0,.015,.90),(.56,.43,.20))
for xx in [-1.33,1.33]:box('Counter oak support',(xx,.0,.438),(.04,.78,.875),oak,.005)
box('Counter low oak shelf',(0,.06,.22),(2.65,.68,.028),oak,.005)
# Sink has a real open bowl.
bowl=box('Central stainless studio sink',(0,.015,.79),(.575,.445,.225),steel,.03)
cut=box('Sink hollow tool',(0,.015,.827),(.54,.410,.245),None,.024)
for b in list(cut.modifiers):bpy.context.view_layer.objects.active=cut;bpy.ops.object.modifier_apply(modifier=b.name)
for b in list(bowl.modifiers):bpy.context.view_layer.objects.active=bowl;bpy.ops.object.modifier_apply(modifier=b.name)
bpy.context.view_layer.objects.active=bowl;b=bowl.modifiers.new('Hollow bowl','BOOLEAN');b.operation='DIFFERENCE';b.object=cut;bpy.ops.object.modifier_apply(modifier=b.name);bpy.data.objects.remove(cut,do_unlink=True)
cyl('Sink drain',(0,.015,.681),.026,.008,steel)
tube('Studio mixer tap',[(0,.32,.922),(0,.32,1.23),(0,.26,1.29),(0,.09,1.29),(0,.06,1.24)],.016,steel)
cyl('Water jar on counter',(.98,.06,.975),.062,.10,clay)
box('Folded wiping cloth',(-.95,-.04,.936),(.36,.26,.028),linen,.013)
# NORTH-WEST WC contains a toilet and basin only, no shower.
current='wc'
for a,b in [(1.74,2.78),(3.58,5.04)]:box('WC brick partition',((a+b)/2,-.40,H/2),(b-a,.10,H),brickx,.002)
box('WC concrete door lintel',(3.18,-.40,2.49),(.80,.10,.62),frame,.003)
for xx in [2.78,3.58]:box('WC door jamb',(xx,-.46,1.08),(.03,.05,2.16),white,.002)
for xx in [2.78,3.58]:
    o=box('Open half-width timber WC door',(xx,-.64,1.07),(.026,.37,2.14),oak,.003)
box('WC concealed cistern',(2.14,.419,.59),(.58,.045,1.18),white,.009)
box('WC wall-hung toilet',(2.14,.10,.35),(.37,.57,.26),white,.12,True)
box('WC toilet seat',(2.14,.06,.495),(.37,.47,.04),white,.11,True)
box('WC flush plate',(2.14,.389,.95),(.18,.009,.12),steel,.01)
box('WC compact oak basin support',(3.19,.22,.59),(.50,.44,.38),oak,.012)
basin=box('WC basin',(3.19,.16,.835),(.48,.39,.10),white,.03)
cut_out(basin,(3.19,.135,.877),(.37,.29,.08))
tube('WC tap',[(3.19,.33,.87),(3.19,.33,1.01),(3.19,.19,1.01)],.010,steel)
box('WC mirror',(3.19,.432,1.49),(.49,.012,.76),mirror,.02)
tube('WC towel rail',[(2.68,.39,1.08),(2.98,.39,1.08)],.007,steel)
box('WC linen towel',(2.83,.355,.93),(.22,.024,.30),linen,.008)
# Artwork textures are the two public links explicitly supplied by the user.
current='art'
def artmaterial(name,filename):
    m=material(name,(.75,.68,.5),.84);img=bpy.data.images.load(str(ROOT/'references'/filename));img.pack()
    tex=m.node_tree.nodes.new('ShaderNodeTexImage');tex.image=img;m.node_tree.links.new(tex.outputs['Color'],m.node_tree.nodes['Principled BSDF'].inputs['Base Color']);return m
artgreen=artmaterial('Margherita artwork - forest green and pale figure','art-web-01.webp')
arthorses=artmaterial('Margherita artwork - pale horses and figures','art-web-02.webp')
def picture(name,center,w,h,m,axis='y',tilt=0):
    x,y,z=center
    if axis=='y':vs=[(x-w/2,y,z-h/2),(x+w/2,y,z-h/2),(x+w/2,y,z+h/2),(x-w/2,y,z+h/2)]
    elif axis=='x':vs=[(x,y+w/2,z-h/2),(x,y-w/2,z-h/2),(x,y-w/2,z+h/2),(x,y+w/2,z+h/2)]
    else:vs=[(x-w/2,y-h/2,z),(x+w/2,y-h/2,z),(x+w/2,y+h/2,z),(x-w/2,y+h/2,z)]
    o=mesh(name,vs,[(0,1,2,3)],m);uv=o.data.uv_layers.new(name='Artwork UV')
    for loop,co in zip(uv.data,[(0,0),(1,0),(1,1),(0,1)]):loop.uv=co
    return o
# Main working easel opposite the wood sink counter.
EX=.35;EY=-5.84
for xx in [EX-.28,EX+.28]:
    limb('Oak easel front leg',(xx,EY+.17,0),(xx,EY,1.88),.027,oak)
limb('Oak easel rear leg',(EX,EY-.38,0),(EX,EY,1.86),.023,oak)
box('Easel canvas shelf',(EX,EY+.055,.94),(.94,.12,.04),oak,.004)
box('Easel working stretched canvas',(EX,EY-.009,1.40),(.82,.036,.91),canvas,.004)
picture('Work in progress - pale horses',(EX,EY+.012,1.40),.79,.88,arthorses)
box('Easel top canvas clamp',(EX,EY+.035,1.9),(.19,.07,.04),oak,.004)
# North studio display: her actual supplied paintings.
box('North painting stretcher',(4.907,-3.32,1.51),(.04,1.28,.86),canvas,.002)
picture('North wall horse painting',(4.884,-3.32,1.51),1.25,.83,arthorses,'x')
box('East painting stretcher',(3.20,-6.943,1.48),(1.05,.04,1.05),canvas,.002)
picture('East wall green painting',(3.20,-6.918,1.48),1.02,1.02,artgreen)
# Canvas drying rack below, with a few cream canvas backs and finished works.
for xx in [2.44,3.96]:box('Drying rack leg',(xx,-6.56,.45),(.04,.52,.9),oak,.004)
box('Canvas drying rack lower rail',(3.20,-6.56,.09),(1.66,.55,.05),oak,.004)
for i in range(5):
    x=2.60+i*.27
    panel=box('Drying canvas back',(x,-6.48,.54),(.045,.62,.86),canvas,.004);panel.rotation_euler[0]=-.08
# Broad making table in south-east bay; crimson working mat echoes the provided art photo.
TX=-2.83;TY=-4.45
box('Large oak art working table',(TX,TY,.865),(2.22,1.03,.045),oak,.013)
for dx in [-.93,.93]:
    for dy in [-.36,.36]:box('Art table charcoal leg',(TX+dx,TY+dy,.42),(.045,.045,.84),black,.004)
box('Burgundy protective work surface',(TX,TY,.890),(2.13,.94,.007),wine,.002)
# Small works lying on the table, with cream grounds and graphic oxide-red forms.
for i,(dx,dy) in enumerate([(-.72,-.23),(-.18,-.23),(.39,-.23),(-.69,.25),(-.15,.25)]):
    cx=TX+dx;cy=TY+dy
    box('Small painted panel',(cx,cy,.906),(.38,.29,.027),canvas,.002)
    for j in range(3):
        pts=[]
        for k in range(25):
            t=k/24;pts.append((cx-.10+t*.20,cy+.02*math.sin(t*9+j)+j*.037-.04,.922))
        tube('Crimson gestural paint marks',pts,.005,paintred)
    if i in [0,3]:
        # Deliberate abstract flame branches, inspired by the supplied small works.
        for j in range(5):
            xx=cx-.075+j*.037;tube('Crimson branching brushwork',[(xx,cy-.02,.924),(xx+.012,cy+.04,.924),(xx-.01,cy+.09,.924)],.0035,paintred)
# One authentic linked artwork as an additional horizontal study on the north table.
box('North art preparation table',(3.26,-3.83,.87),(1.85,.92,.05),oak,.01)
for dx in [-.72,.72]:
    for dy in [-.3,.3]:box('Preparation trestle',(3.26+dx,-3.83+dy,.422),(.05,.05,.844),oak,.004)
box('Green artwork study lying flat',(3.02,-3.78,.914),(.51,.51,.035),canvas,.003)
picture('Green study paint surface',(3.02,-3.78,.934),.49,.49,artgreen,'z')
# Palette, jars, brushes and paint tubes: visibly working, with circulation left open.
def brushpot(x,y,z):
    cyl('Stoneware brush pot',(x,y,z+.065),.060,.13,clay)
    cyl('Dark opening in brush pot',(x,y,z+.132),.052,.004,black)
    for i in range(6):
        dx=(i%3-1)*.022;dy=(i//3-.5)*.023
        tube('Long handled artist brush',[(x+dx,y+dy,z+.09),(x+dx*1.6,y+dy*1.6,z+.33+(.02 if i%2 else 0))],.003,oak)
        limb('Brush bristle',(x+dx*1.6,y+dy*1.6,z+.31),(x+dx*1.6,y+dy*1.6,z+.345),.005,paintred if i%2 else green)
brushpot(TX+.83,TY+.26,.896);brushpot(EX+.54,EY+.30,.79)
# Small rolling paint cart beside the artist.
for xx in [.82,1.20]:
    for yy in [-5.7,-5.38]:
        limb('Studio trolley upright',(xx,yy,.12),(xx,yy,.79),.014,black);cyl('Trolley caster',(xx,yy,.06),.035,.025,black,(math.pi/2,0,0))
for zz in [.2,.76]:box('Studio trolley oak shelf',(1.01,-5.54,zz),(.48,.41,.024),oak,.004)
for i,m in enumerate([paintred,green,white,blue]):
    cyl('Open paint jar',(.88+(i%2)*.16,-5.49-(i//2)*.16,.80),.041,.07,m)
for i in range(5):
    o=box('Paint tube',(TX+.68+(i%2)*.09,TY-.18+(i//2)*.09,.919),(.065,.028,.025),white,.007);o.rotation_euler.z=.3
    cyl('Paint tube cap',(TX+.65+(i%2)*.09,TY-.18+(i//2)*.09,.923),.01,.016,paintred,(0,math.pi/2,0))
palette=sphere('Paint mixing palette',(TX+.30,TY+.25,.91),(.18,.12,.015),oak)
for i,m in enumerate([paintred,green,white,blue]):sphere('Paint on palette',(TX+.20+i*.06,TY+.27,.927),(.023,.018,.006),m)
# Simple stool in active studio.
cyl('Oak artist stool',(-1.91,-3.46,.51),.19,.055,oak)
for a in [30,150,270]:
    t=math.radians(a);limb('Stool leg',(-1.91+.17*math.cos(t),-3.46+.17*math.sin(t),.02),(-1.91+.11*math.cos(t),-3.46+.11*math.sin(t),.49),.018,oak)
# Stylized adult artist, sculptural and explicitly non-photoreal.
current='person'
skin=material('Stylized warm clay skin',(.57,.34,.23),.85)
hair=material('Golden blonde sculpted hair',(.45,.28,.11),.84)
roots=material('Soft dark hair roots',(.15,.086,.044),.88)
shirt=material('Margherita black studio shirt',(.022,.024,.024),.9)
shorts=material('Margherita grey studio shorts',(.17,.145,.13),.94)
gold=material('Thin round gold spectacles',(.50,.32,.12),.25,.7)
ink=material('Stylized arm tattoo ink',(.033,.045,.033),.88)
PX=.01;PY=-5.07
def P(a,b,c):return (PX+a,PY+b,c)
# Neutral natural adult proportions, not a lifelike face.
for dx in [-.115,.115]:
    box('Artist soft black shoe',P(dx,-.045,.065),(.115,.235,.12),shirt,.046,True)
    limb('Artist lower leg',P(dx,0,.14),P(dx+.008,-.01,.49),.046,skin)
    limb('Artist thigh',P(dx+.008,-.01,.49),P(dx,.01,.84),.061,skin)
    box('Artist shorts leg',P(dx,0,.82),(.17,.20,.28),shorts,.055,True)
sphere('Artist hips',P(0,0,.94),(.19,.12,.15),shorts)
sphere('Artist black shirt torso',P(0,-.015,1.15),(.205,.12,.28),shirt)
limb('Artist neck',P(0,-.012,1.37),P(0,-.023,1.47),.044,skin)
head=sphere('Artist stylized face',P(0,-.037,1.555),(.088,.084,.115),skin)
sphere('Artist subtle nose',P(0,-.12,1.55),(.014,.022,.024),skin)
# Eyes and a quiet smile remain minimal sculptural marks.
for xx in [-.038,.038]:
    sphere('Artist simple eye',P(xx,-.119,1.574),(.004,.003,.004),ink)
    pts=[P(xx+.041*math.cos(t),-.122,1.575+.038*math.sin(t)) for t in [k*math.tau/48 for k in range(49)]]
    tube('Round gold glasses',pts,.002,gold)
tube('Glasses bridge',[P(-.007,-.124,1.575),P(.007,-.124,1.575)],.002,gold)
tube('Artist subtle smile',[P(-.018,-.118,1.515),P(0,-.121,1.512),P(.018,-.118,1.515)],.0015,ink)
sphere('Artist dark hair crown',P(0,.003,1.615),(.094,.082,.067),roots)
# Long wavy blonde hair, grouped in soft strands down her back.
for i in range(15):
    a=math.pi*(.03+.94*i/14);xx=.095*math.cos(a);yy=.025+.075*math.sin(a)
    pts=[]
    for j in range(14):
        t=j/13;pts.append(P(xx+(math.sin(t*8+i)*.013)*t,yy+.025*t+math.sin(t*7+i)*.007,1.645-.53*t))
    tube('Sculpted blonde hair strand',pts,.013+(.003 if i%3==0 else 0),hair)
# Right hand raises a brush to the easel; left hand holds the paint palette.
Rshoulder=P(.18,-.015,1.30);Relbow=P(.30,-.27,1.20);Rhand=P(.27,-.56,1.43)
Lshoulder=P(-.18,-.015,1.30);Lelbow=P(-.29,-.13,1.08);Lhand=P(-.19,-.30,1.11)
for label,a,b,c in [('Right painting',Rshoulder,Relbow,Rhand),('Left palette',Lshoulder,Lelbow,Lhand)]:
    limb(label+' sleeve',a,Vector(a).lerp(Vector(b),.4),.06,shirt)
    limb(label+' upper arm',Vector(a).lerp(Vector(b),.35),b,.041,skin);sphere(label+' elbow',b,(.041,.04,.04),skin)
    limb(label+' forearm',b,c,.030,skin);sphere(label+' hand',c,(.035,.028,.044),skin)
tube('Brush held to painting',[Rhand,(EX-.03,EY+.035,1.47)],.004,oak)
limb('Loaded brush tip',(EX-.03,EY+.04,1.47),(EX-.025,EY+.012,1.47),.006,paintred)
sphere('Handheld artist palette',(Lhand[0],Lhand[1]-.025,Lhand[2]-.03),(.12,.08,.014),oak)
for i,m in enumerate([paintred,green,white]):sphere('Paint on handheld palette',(Lhand[0]-.065+i*.055,Lhand[1]-.04,Lhand[2]-.014),(.018,.016,.005),m)
# Graphic tattoo marks on the visible arm, deliberately simplified.
for j in range(8):
    t=.2+j*.075;v=Vector(Lshoulder).lerp(Vector(Lelbow),t)
    tube('Artist illustrative tattoo',[(v.x-.038,v.y-.02,v.z-.025),(v.x-.046,v.y-.042,v.z),(v.x-.04,v.y-.02,v.z+.025)],.0028,ink)
# Lighting: soft daylight through the existing end windows.
current='lights'
def area(name,pos,power,color,size,target,sy):
    d=bpy.data.lights.new(name,'AREA');d.energy=power;d.color=color;d.shape='RECTANGLE';d.size=size;d.size_y=sy;o=bpy.data.objects.new(name,d);cols[current].objects.link(o);o.location=pos;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();return o
area('South window daylight',(-5.4,-3.3,1.75),1400,(1,.93,.83),5,(0,-3.3,1),1.5)
area('North window daylight',(5.65,-3.35,1.75),1800,(.89,.95,1),4.4,(1,-4,1),1.5)
area('Soft broad studio fill',(0,-3.5,2.65),340,(1,.96,.88),4,(0,-4,0),2)
area('Bathroom ceiling light',(3.45,.03,2.69),70,(1,.94,.85),1.2,(3.1,0,.5),.45)
emit=material('Warm diffused task light',(1,.78,.5),.6);pp=emit.node_tree.nodes['Principled BSDF'];pp.inputs['Emission Color'].default_value=(1,.84,.63,1);pp.inputs['Emission Strength'].default_value=2
current='wc';box('Small WC ceiling diffuser',(3.2,.025,2.78),(.68,.31,.02),emit,.005);current='lights'
world=bpy.data.worlds.new('Neutral studio daylight');world.use_nodes=True;world.node_tree.nodes['Background'].inputs[0].default_value=(.72,.79,.89,1);world.node_tree.nodes['Background'].inputs[1].default_value=.4;scene.world=world
shots=[
('01_studio_from_stairs',(-3.80,-1.25,1.66),(.65,-4.1,1.19),24),
('02_margherita_painting',(1.24,-5.30,1.62),(.18,-5.48,1.30),23),
('03_art_worktable',(-4.18,-2.60,1.82),(-1.18,-4.87,.95),30),
('04_wood_sink_counter',(1.05,-2.85,1.62),(-.05,-.015,.88),26),
('05_south_making_space',(1.12,-3.8,1.70),(-3.20,-3.9,1.03),24),
('06_toilet_and_basin',(4.56,.005,1.65),(2.52,.03,.88),20),
('07_artist_and_paintings',(3.42,-2.76,1.68),(.48,-5.69,1.16),34),
('08_studio_floorplan',(-10.5,-13,14),(.16,-3.25,0),48)]
cameras=[]
for name,loc,target,lens in shots:
    d=bpy.data.cameras.new(name);o=bpy.data.objects.new(name,d);cols['lights'].objects.link(o);o.location=loc;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();d.lens=lens;d.clip_start=.025;d.clip_end=100
    if name.startswith('08'):d.type='ORTHO';d.ortho_scale=18.0
    cameras.append(o)
scene.camera=cameras[0];scene.render.engine='CYCLES';scene.cycles.samples=int(os.getenv('STUDIO_SAMPLES','64'));scene.cycles.use_denoising=True;scene.cycles.max_bounces=8
scene.render.resolution_x=int(os.getenv('STUDIO_RESOLUTION','1800'));scene.render.resolution_y=round(scene.render.resolution_x*2/3);scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG';scene.render.image_settings.color_mode='RGB';scene.view_settings.view_transform='AgX';scene.view_settings.exposure=-.35
try:
    prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
    if any(d.type=='METAL' for d in prefs.devices):
        for d in prefs.devices:d.use=d.type=='METAL'
        scene.cycles.device='GPU'
except Exception:pass
record={'floor':'First floor / nivo 1 / active art and making studio','ceiling_height_m':2.8,'bay_lengths_south_to_north_m':[2.90,2.88,3.30],'frame_thicknesses_m':[.37,.30],'finishes':['exposed red-brown brick walls','honed grey concrete floor','exposed concrete piers and transverse beams','flat light concrete ceiling','natural oak sink counter'],'layout':['south-west winding stair kept in the same footprint','simple 2.85 m wood counter with central sink in west middle bay','north-west toilet and basin only, no shower','east-side easel with stylized Margherita actively painting','south-east making table with small works and paint supplies','north artwork display and preparation table'],'person':'Non-photoreal sculptural adult figure inspired by user-supplied Margherita photos: long blonde hair, round glasses, dark studio shirt, grey shorts, simplified arm tattoos. Painting brush contacts the easel canvas. Reference photos are not embedded.','artwork':'Two user-supplied linked paintings are packed into the model; tabletop crimson studies are illustrative interpretations of the attached artwork photo.','assumptions':['Envelope width and five end-window positions retained from the existing model as requested; bay lengths follow the new first-floor plan.','Window sill 570 mm and head 2270 mm are illustrative as no first-floor elevation was supplied.','Concrete beam downstands assumed 300 mm below the 2800 mm ceiling.','Stair winder geometry and heights remain a visualization interpretation, not shop drawings.','Figure height, furniture and equipment placement are illustrative; original personal photos and plan screenshot are private references.']}
(ROOT/'model/specification.json').write_text(json.dumps(record,indent=2)+'\n')
t=bpy.data.texts.new('Studio - dimensions and assumptions');t.write(json.dumps(record,indent=2))
bpy.ops.object.select_all(action='DESELECT')
for screen in bpy.data.screens:
    for a in screen.areas:
        if a.type=='VIEW_3D':a.spaces.active.region_3d.view_perspective='CAMERA';a.spaces.active.overlay.show_overlays=False;a.spaces.active.shading.type='MATERIAL'
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'model/studio.blend'))
if os.getenv('STUDIO_RENDER','1')=='1':
    selected=os.getenv('STUDIO_SHOTS','')
    for cam in cameras:
        if selected and cam.name[:2] not in selected.split(','):continue
        hidden=[]
        if cam.name.startswith('08'):
            for o in scene.objects:
                if o in cols['ceiling'].objects[:] or o.name.startswith(('East exposed','South','WC brick partition','WC concrete door lintel','Open half-width timber','WC door jamb','Concrete transverse downstand','Small WC ceiling diffuser')):
                    if not o.hide_render:o.hide_render=True;hidden.append(o)
            top=area('Cutaway softbox',(0,-3,11),2800,(1,.97,.91),8,(0,-3,0),8)
        scene.camera=cam;scene.render.filepath=str(ROOT/'reference-views'/f'{cam.name}.png');bpy.ops.render.render(write_still=True)
        for o in hidden:o.hide_render=False
        if cam.name.startswith('08'):bpy.data.objects.remove(top,do_unlink=True)
scene.camera=cameras[0];bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'model/studio.blend'))
bpy.ops.object.select_all(action='DESELECT')
for o in list(scene.objects):
    if o.type in {'MESH','CURVE'}:o.select_set(True)
for o in list(bpy.context.selected_objects):
    if o.type=='CURVE':
        dg=bpy.context.evaluated_depsgraph_get();m=bpy.data.meshes.new_from_object(o.evaluated_get(dg),depsgraph=dg);n=bpy.data.objects.new(o.name,m)
        for c in o.users_collection:c.objects.link(n)
        n.matrix_world=o.matrix_world.copy();n.select_set(True);bpy.data.objects.remove(o,do_unlink=True)
bpy.ops.export_scene.gltf(filepath=str(ROOT/'model/studio.glb'),export_format='GLB',use_selection=True,export_apply=True)
print('STUDIO_COMPLETE',ROOT,flush=True)

