Preserve homestead context and garden-centred Blender design
This commit is contained in:
218
blender/scripts/add_daily_life.py
Normal file
218
blender/scripts/add_daily_life.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import bpy,math,os,json
|
||||
from mathutils import Vector,Matrix
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene
|
||||
detail=bpy.data.collections['DETAIL - full geometry for renders']
|
||||
col=bpy.data.collections.new('33 Daily life - training gathering play and food');detail.children.link(col)
|
||||
COL={'life':col};active='life'
|
||||
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
|
||||
cubefs=[tuple(reversed(f)) for f in cubefs]
|
||||
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];roof=bpy.data.materials['Pale Colorbond - powder coated steel'];linen=bpy.data.materials['Undyed flax linen'];gravel=bpy.data.materials['Loose local gravel - fine stones'];F=1.12
|
||||
rubber=mat('Exercise mat muted charcoal',(.035,.048,.044),.94)
|
||||
sage=mat('Table tennis muted green',(.10,.21,.16),.63)
|
||||
cream=mat('Ceramic tea bowls warm white',(.73,.68,.56),.48)
|
||||
white=mat('Table tennis painted court lines',(.8,.79,.71),.63)
|
||||
black=mat('Freezer seals and equipment grips',(.028,.03,.029),.7)
|
||||
metal=mat('Appliance brushed metal',(.44,.46,.43),.36,.7)
|
||||
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
||||
def path(n,pts,w):
|
||||
vs=[];fs=[]
|
||||
for a,b in zip(pts,pts[1:]):
|
||||
a,b=Vector(a),Vector(b);delta=b-a;perp=Vector((-delta.y,delta.x)).normalized()*w/2;steps=max(2,int(delta.length/.3));off=len(vs)
|
||||
for i in range(steps+1):
|
||||
q=a+delta*i/steps
|
||||
for v in [q-perp,q+perp]:vs.append((v.x,v.y,ground(v.x,v.y)+.085))
|
||||
for i in range(steps):k=off+2*i;fs.append((k,k+1,k+3,k+2))
|
||||
return mesh(n,vs,fs,gravel)
|
||||
def deck(n,cx,cy,w,d,z):
|
||||
box(n+' accessible pier deck',(cx,cy,z-.09),(w,d,.18),wood,.01)
|
||||
for x in [cx-w/2+.15,cx+w/2-.15]:
|
||||
for y in [cy-d/2+.15,cy+d/2-.15]:
|
||||
g=ground(x,y);box(n+' stone pier',(x,y,(g+z-.18)/2),(.3,.3,max(.08,z-.18-g)),stone,.01)
|
||||
for i in range(int(d/.15)):
|
||||
box(n+' deck board joint',(cx,cy-d/2+.075+i*.15,z+.002),(w,.006,.003),steel)
|
||||
def bench(n,x,y,z,length=2.7):
|
||||
box(n+' seat',(x,y,z+.45),(length,.43,.065),wood,.015)
|
||||
box(n+' linen seat pad',(x,y,z+.502),(length-.1,.39,.04),linen,.016)
|
||||
for xx in [x-length*.37,x+length*.37]:box(n+' leg',(xx,y,z+.21),(.07,.35,.42),woodz,.008)
|
||||
box(n+' backrest',(x,y+.21,z+.75),(length,.055,.27),wood,.012)
|
||||
def ring(n,c,r,t,material):
|
||||
cx,cy,cz=c;vs=[];fs=[];N=48;M=10
|
||||
for i in range(N):
|
||||
a=i*2*pi/N
|
||||
for j in range(M):
|
||||
b=j*2*pi/M;rr=r+t*cos(b);vs.append((cx+rr*cos(a),cy+t*sin(b),cz+rr*sin(a)))
|
||||
for i in range(N):
|
||||
for j in range(M):fs.append((i*M+j,((i+1)%N)*M+j,((i+1)%N)*M+(j+1)%M,i*M+(j+1)%M))
|
||||
ob=mesh(n,vs,fs,material)
|
||||
for p in ob.data.polygons:p.use_smooth=True
|
||||
return ob
|
||||
# Covered exercise pavilion west of the garage. Independent steel exercise frame.
|
||||
tx,ty,tz=-12.2,-18,.95
|
||||
deck('TRAINING',tx,ty,4.8,5.4,tz)
|
||||
for x in [tx-2.28,tx+2.28]:
|
||||
for y,h in [(ty-2.57,2.95),(ty+2.57,3.25)]:
|
||||
box('Training timber canopy post',(x,y,tz+h/2),(.125,.125,h),woodz,.005)
|
||||
box('Training canopy steel shoe',(x,y,tz+.07),(.16,.16,.14),steel,.005)
|
||||
for x in [tx-2.28+i*.57 for i in range(9)]:beam('Training exposed roof rafter',(x,ty-2.9,tz+2.95),(x,ty+2.9,tz+3.29),.065,.18,wood)
|
||||
ro=box('Training pale mono pitch canopy',(tx,ty,tz+3.16),(5.3,6,.065),roof,.006);ro.rotation_euler.x=math.atan(.3/5.4)
|
||||
for x in [tx-.98,tx+.98]:
|
||||
box('EXERCISE FRAME - steel upright',(x,ty+.85,tz+1.4),(.095,.095,2.8),steel,.006)
|
||||
box('Exercise frame bolted foot plate',(x,ty+.85,tz+.02),(.30,.36,.04),steel,.005)
|
||||
for dx in [-.1,.1]:
|
||||
for dy in [-.12,.12]:rod('Exercise frame anchor concept',(x+dx,ty+.85+dy,tz+.04),(x+dx,ty+.85+dy,tz+.067),.011,metal,sides=8)
|
||||
bar=rod('PULL UP BAR - dedicated steel frame',(tx-1.02,ty+.85,tz+2.70),(tx+1.02,ty+.85,tz+2.70),.017,steel,sides=20)
|
||||
bar['Design intent']='Dedicated exercise frame intended to be load rated by its manufacturer or structural engineer. These concept anchor plates do not establish a safe working load.'
|
||||
for x in [tx-.28,tx+.28]:
|
||||
box('Adjustable ring hanging strap',(x,ty+.85,tz+2.10),(.027,.009,1.18),black,.002)
|
||||
ring('WOODEN GYMNASTIC RING',(x,ty+.85,tz+1.40),.125,.017,wood)
|
||||
for x,y in [(tx-.65,ty-.5),(tx+.55,ty-.5)]:box('MOVABLE EXERCISE MAT',(x,y,tz+.022),(.85,1.85,.045),rubber,.025)
|
||||
box('Adjustable weight bench seat',(tx+1.55,ty-.88,tz+.45),(.40,.38,.09),rubber,.035)
|
||||
back=box('ADJUSTABLE BENCH - inclined back',(tx+1.55,ty-.25,tz+.60),(.40,.91,.09),rubber,.035);back.rotation_euler.x=.3
|
||||
beam('Bench adjustable steel spine',(tx+1.55,ty-1.05,tz+.15),(tx+1.55,ty+.23,tz+.41),.065,.065,steel)
|
||||
for y in [ty-1,ty+.15]:box('Bench stabilising foot',(tx+1.55,y,tz+.065),(.59,.065,.065),steel,.009)
|
||||
box('Dumbbell timber stand',(tx-1.55,ty-.5,tz+.72),(.68,1.25,.06),wood,.012)
|
||||
for y in [ty-.95,ty-.05]:
|
||||
box('Dumbbell stand leg',(tx-1.55,y,tz+.34),(.5,.06,.68),steel)
|
||||
for y in [ty-.80,ty-.22]:
|
||||
rod('ADJUSTABLE DUMBBELL grip',(tx-1.73,y,tz+.85),(tx-1.37,y,tz+.85),.019,steel,sides=16)
|
||||
for x in [tx-1.77,tx-1.33]:
|
||||
for dx in [-.06,-.02,.02,.06]:box('Dumbbell selectable weight plate',(x+dx,y,tz+.85),(.027,.22,.23),black,.008)
|
||||
for i,x in enumerate([-9.60,-9.25,-8.90]):
|
||||
top=.76-.19*i;box('Training access step',(x,-17,top-.08),(.36,1.2,.16),wood,.009)
|
||||
path('Training to garage pedestrian link',[(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],1.1)
|
||||
# Outdoor dining: a separate terrace keeps the original narrow veranda clear.
|
||||
dx,dy=10,3.5;deck('GATHERING TERRACE',dx,dy,6,5.8,F)
|
||||
deck('Veranda terrace connection',6.5,4.2,1,1.5,F)
|
||||
box('OUTDOOR TABLE - ten place timber top',(dx,dy,F+.76),(3.2,1.03,.085),wood,.025)
|
||||
for x in [dx-1.13,dx+1.13]:box('Outdoor table trestle',(x,dy,F+.36),(.10,.78,.72),woodz,.01)
|
||||
bench('Outdoor north bench - four places',dx,dy+.98,F,2.95)
|
||||
south_start=set(col.objects);bench('Outdoor south bench - four places',dx,dy-.98,F,2.95)
|
||||
for o in set(col.objects)-south_start:
|
||||
if 'backrest' in o.name:o.location.y=dy-1.19
|
||||
for x in [dx-1.94,dx+1.94]:
|
||||
box('Outdoor end chair seat',(x,dy,F+.45),(.47,.47,.06),wood,.012)
|
||||
for a in [-.17,.17]:
|
||||
for b in [-.17,.17]:box('Outdoor end chair leg',(x+a,dy+b,F+.21),(.038,.038,.42),woodz)
|
||||
box('Outdoor end chair back',(x+(-.21 if x<dx else .21),dy,F+.75),(.055,.47,.36),wood,.01)
|
||||
for x in [7.15,12.85]:
|
||||
for y in [.75,6.25]:box('Gathering pergola post',(x,y,F+1.38),(.12,.12,2.76),woodz,.006)
|
||||
for y in [.75,6.25]:box('Gathering pergola beam',(10,y,F+2.76),(6,.12,.20),wood,.005)
|
||||
for x in [7.1+i*.35 for i in range(18)]:box('Gathering pergola shade slat',(x,3.5,F+2.88),(.065,5.8,.14),wood)
|
||||
# Nearby movable tea counter and shared bowls.
|
||||
box('TEA AND MATCHA - timber preparation cart',(11.6,5.74,F+.88),(1.55,.58,.06),wood,.016)
|
||||
for x in [11,12.2]:
|
||||
for y in [5.55,5.93]:box('Tea cart frame',(x,y,F+.43),(.045,.045,.86),woodz)
|
||||
box('Tea cart storage shelf',(11.6,5.74,F+.25),(1.46,.50,.04),wood)
|
||||
rod('Tea kettle',(11.15,5.72,F+.92),(11.15,5.72,F+1.13),.10,metal,sides=24)
|
||||
for x in [11.5,11.76,12.02]:rod('Matcha ceramic bowl',(x,5.73,F+.92),(x,5.73,F+1.01),.075,cream,r2=.095,sides=24)
|
||||
for x in [8.9,9.7,10.5,11.3]:
|
||||
for y in [dy-.28,dy+.28]:rod('Shared table cup',(x,y,F+.81),(x,y,F+.89),.037,cream,sides=16)
|
||||
# Table tennis has its own level deck, clear of vehicles; folding halves remain editable.
|
||||
px,py,pz=11,-8.5,.80;deck('PLAY DECK',px,py,5.5,6.5,pz)
|
||||
for y in [py-.695,py+.695]:
|
||||
box('FOLDING TABLE TENNIS - hinged half',(px,y,pz+.76),(1.525,1.37,.045),sage,.004)
|
||||
for x in [px-.50,px+.50]:
|
||||
box('Table tennis folding leg',(x,y,pz+.38),(.035,.035,.71),steel,.004)
|
||||
rod('Table tennis caster',(x-.025,y,pz+.035),(x+.025,y,pz+.035),.034,black,sides=12)
|
||||
for x in [px-.746,px+.746,px]:box('Table tennis white longitudinal line',(x,py,pz+.784),(.012,2.73,.002),white)
|
||||
for y in [py-1.36,py+1.36]:box('Table tennis white end line',(px,y,pz+.785),(1.52,.018,.002),white)
|
||||
for x in [px-.82,px+.82]:rod('Table tennis net post',(x,py,pz+.76),(x,py,pz+.915),.007,steel,sides=8)
|
||||
for i in range(7):rod('Table tennis horizontal net cord',(px-.82,py,pz+.77+i*.024),(px+.82,py,pz+.77+i*.024),.0015,white,sides=5)
|
||||
for i in range(43):rod('Table tennis vertical net cord',(px-.8+i*.038,py,pz+.77),(px-.8+i*.038,py,pz+.914),.0012,white,sides=5)
|
||||
for i,x in enumerate([8.05,7.70,7.35]):
|
||||
top=.62-.18*i;box('Play deck access step',(x,-7,top-.08),(.36,1.2,.16),wood,.009)
|
||||
path('Play deck pedestrian connection',[(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],1.1)
|
||||
# A reserved footprint only: no court surface or working hoop is built now.
|
||||
future=bpy.data.materials['Future shadow - faint sage outline']
|
||||
for a,b in [((8,-41),(18,-41)),((18,-41),(18,-31)),((18,-31),(8,-31)),((8,-31),(8,-41))]:
|
||||
a,b=Vector(a),Vector(b)
|
||||
for i in range(10):
|
||||
p=a+(b-a)*i/10;q=a+(b-a)*(i+.55)/10
|
||||
beam('FUTURE BASKETBALL - dashed reserve',(p.x,p.y,ground(p.x,p.y)+.09),(q.x,q.y,ground(q.x,q.y)+.09),.07,.04,future)
|
||||
# Walking loop: joins the quiet room and garden without crossing building reserves.
|
||||
loop=[(-11,14.7),(-14,18),(-11,23),(1,24),(13,23),(20,19),(19,10),(30,10),(33,3),(33,-10),(27,-13),(20,-11),(17,-4),(14,1),(14,10),(19,10)]
|
||||
path('SHADED WALKING LOOP - garden and quiet room',loop,1.05)
|
||||
path('Walking loop return to home',[(14,1),(13,4),(13,7),(6,7)],1.05)
|
||||
path('Walking loop home to meditation',[(-1,24),(-7,21),(-14,18)],1.05)
|
||||
bench('QUIET GARDEN BENCH',30,10.9,ground(30,10.9)+.07,1.8)
|
||||
for x in [29,31]:box('Quiet bench shade post',(x,11,ground(x,11)+1.2),(.10,.10,2.4),woodz,.006)
|
||||
for x in [28.9+i*.26 for i in range(9)]:box('Quiet bench timber shade slat',(x,11,ground(30,11)+2.45),(.06,1.6,.10),wood)
|
||||
# Everyday food: clear prep counter, pantry and compact freezer along the south wall.
|
||||
box('BREAKFAST - clear preparation counter',(.22,-2.52,F+.94),(1.15,.73,.055),stone,.013)
|
||||
box('Breakfast counter timber base',(.22,-2.55,F+.45),(1.10,.62,.89),wood,.009)
|
||||
box('Breakfast chopping board',(.30,-2.48,F+.982),(.42,.30,.025),wood,.008)
|
||||
for x in [-.19,.02]:rod('Breakfast cereal jar',(x,-2.73,F+.98),(x,-2.73,F+1.18),.066,cream,sides=20)
|
||||
box('PANTRY - tall timber cabinet',(1.32,-2.55,F+1.06),(.85,.65,2.12),wood,.014)
|
||||
for x in [1.12,1.52]:box('Pantry door pull',(x,-2.21,F+1.05),(.023,.028,.23),steel,.004)
|
||||
box('FREEZER - compact upright',(2.27,-2.55,F+.66),(.72,.69,1.32),cream,.025)
|
||||
box('Freezer insulated door',(2.27,-2.19,F+.68),(.68,.05,1.23),metal,.014)
|
||||
box('Freezer door handle',(2.52,-2.14,F+.89),(.025,.04,.32),steel,.007)
|
||||
for x in [2.04,2.5]:box('Freezer ventilation clearance foot',(x,-2.54,F+.035),(.065,.48,.07),black)
|
||||
hx,hy=29.8,1.5;hz=ground(hx,hy)+.08
|
||||
box('HARVEST BENCH - garden sorting surface',(hx,hy,hz+.90),(.78,2.05,.065),wood,.018)
|
||||
for x in [hx-.28,hx+.28]:
|
||||
for y in [hy-.81,hy+.81]:box('Harvest bench leg',(x,y,hz+.43),(.06,.06,.86),woodz,.006)
|
||||
box('Harvest bench lower storage',(hx,hy,hz+.22),(.70,1.95,.045),wood)
|
||||
for y in [hy-.6,hy+.10]:
|
||||
box('Harvest timber produce crate',(hx,y,hz+1.045),(.57,.48,.24),wood,.012)
|
||||
box('Harvest crate dark interior',(hx,y,hz+1.171),(.49,.4,.01),bpy.data.materials['Damp brown garden soil'])
|
||||
layout={'training':[-12.2,-18,4.8,5.4],'outdoor_table':[10,3.5,6,5.8],'table_tennis':[11,-8.5,5.5,6.5],'future_basketball':[13,-36,10,10],'walking_loop':loop,'harvest_bench':[29.8,1.5], 'outdoor_seats':10}
|
||||
json.dump(layout,open(OUT+'/daily-life-layout.json','w'),indent=2)
|
||||
exec(compile(open(OUT+'/enhance_kitchen.py').read(),OUT+'/enhance_kitchen.py','exec'))
|
||||
# A single merged viewport copy for new geometry.
|
||||
verts=[];faces=[];mi=[];mats=[];mapping={}
|
||||
for o in col.objects:
|
||||
if o.type!='MESH':continue
|
||||
remap=[]
|
||||
for m in o.data.materials:
|
||||
if m not in mapping:mapping[m]=len(mats);mats.append(m)
|
||||
remap.append(mapping[m])
|
||||
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
|
||||
verts.extend(tuple(world@v.co) for v in o.data.vertices)
|
||||
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mi.append(remap[p.material_index])
|
||||
me=bpy.data.meshes.new('Daily life lightweight display');me.from_pydata(verts,[],faces);me.update()
|
||||
for m in mats:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',mi)
|
||||
proxy=bpy.data.objects.new('FAST | Training gathering play and garden loop',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
shots=[('25 / TRAINING & STRENGTH',(-8,-12,4),(-8.8,-12.8,3.8),(-12.2,-18,2),25),('26 / OUTDOOR TABLE & MATCHA',(15,9,4.8),(14,8,4.4),(10,3.5,1.6),28),('27 / PLAY & FUTURE BASKETBALL',(17,-1,5),(16,-2,4.7),(11,-8.5,1),29),('28 / WALKING & FOOD GARDEN',(35,16,8),(33,14,7),(27,3,1.3),30),('29 / EASY EVERYDAY FOOD',(-.3,.4,2.9),(.1,.1,2.8),(1,-2.5,2),24)]
|
||||
caption_col=bpy.data.collections['22 PLAY - guided camera tour']
|
||||
ref=max((o for o in caption_col.objects if o.type=='FONT'),key=lambda o:o.name)
|
||||
ref.scale=(0,0,0);ref.keyframe_insert('scale',frame=2881)
|
||||
shots.append(('30 / COPPER & MARBLE KITCHEN',(-.8,2.5,3.1),(-1.0,2.2,2.95),(-3.45,-1.2,1.9),25))
|
||||
chapters=json.load(open(OUT+'/tour-chapters-reading.json'))
|
||||
for idx,(title,a,b,target,lens) in enumerate(shots):
|
||||
start=2881+idx*120;end=start+119
|
||||
for f,p in [(start,a),(end,b)]:
|
||||
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=f);cam.keyframe_insert('rotation_euler',frame=f);cam.data.lens=lens;cam.data.keyframe_insert('lens',frame=f)
|
||||
caption=ref.copy();caption.data=ref.data.copy();caption.animation_data_clear();caption.data.body=title;caption.name='Tour chapter '+title;caption_col.objects.link(caption)
|
||||
half=36/lens;caption.location=(-half*.91,half*9/16*.84,-2)
|
||||
for f,sz in [(1,0),(start-1,0),(start,1),(end,1),(end+1,0)]:
|
||||
caption.scale=(sz,sz,sz);caption.keyframe_insert('scale',frame=f)
|
||||
s.timeline_markers.new(title,frame=start);chapters.append({'title':title,'start':start,'end':end,'seconds':5})
|
||||
for title in caption_col.objects:
|
||||
if title.type=='FONT' and title.animation_data:
|
||||
for layer in title.animation_data.action.layers:
|
||||
for strip in layer.strips:
|
||||
for bag in strip.channelbags:
|
||||
for fc in bag.fcurves:
|
||||
if fc.data_path=='scale':
|
||||
for k in fc.keyframe_points:k.interpolation='CONSTANT'
|
||||
s.frame_end=3600;s.frame_set(3481);s.camera=cam
|
||||
json.dump(chapters,open(OUT+'/tour-chapters-daily-life.json','w'),indent=2)
|
||||
s['Daily life additions']='Covered exercise area, ten-place outdoor table and tea counter, folding table tennis, unbuilt basketball reserve, garden walking loop and quiet bench, breakfast counter, pantry/freezer and harvest bench.'
|
||||
s['Training design status']='Concept geometry only: dedicated exercise frame requires appropriate manufacturer rating/engineering and equipment clearances; future basketball rectangle is a reserved area, not a built level court.'
|
||||
report={'training_area':1,'pullup_bar':1,'wooden_rings':2,'adjustable_dumbbells':2,'adjustable_bench':1,'exercise_mats':2,'outdoor_seats':10,'tea_counter':1,'folding_table_tennis':1,'basketball_status':'Future reserve only','walking_loop':True,'quiet_bench':1,'breakfast_counter':1,'pantry':1,'freezer':1,'harvest_bench':1,'tour_chapters':30,'tour_seconds':150,'kitchen':kitchen_checks}
|
||||
json.dump(report,open(OUT+'/daily-life-checks.json','w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-daily-life-tour.blend')
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT':o.hide_render=True
|
||||
s.render.resolution_percentage=60;s.cycles.samples=24;s.cycles.use_denoising=True
|
||||
prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
|
||||
for d in prefs.devices:d.use=d.type=='METAL'
|
||||
s.cycles.device='GPU'
|
||||
for fname,frame in [('training-area.png',2920),('outdoor-gathering.png',3035),('kitchen-copper-marble.png',3520)]:
|
||||
s.frame_set(frame);s.render.filepath=OUT+'/'+fname;bpy.ops.render.render(write_still=True)
|
||||
s.frame_set(3300);cam.animation_data_clear();cam.data.animation_data_clear();cam.location=(48,55,48);cam.rotation_euler=(Vector((2,-8,0))-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.lens=37;s.render.filepath=OUT+'/daily-life-overview.png';bpy.ops.render.render(write_still=True)
|
||||
print('DAILY_LIFE_READY '+json.dumps(report),flush=True)
|
||||
64
blender/scripts/create_site_plan.py
Normal file
64
blender/scripts/create_site_plan.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from pathlib import Path
|
||||
from html import escape
|
||||
import json
|
||||
O=Path(__file__).parent
|
||||
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1120" viewBox="0 0 1400 1120"><rect width="1400" height="1120" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}</style><text x="60" y="60" font-size="32" font-weight="600">A home for movement, connection and quiet.</text><text x="60" y="95" font-size="17">SAMFORD VALLEY / Updated concept plan · 10 September 2026 · Version 003</text>']
|
||||
def pt(x,y):return (630+13*x,470-13*y)
|
||||
def label(x,y,lines,size=12):
|
||||
X,Y=pt(x,y)
|
||||
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
|
||||
def rect(cx,cy,w,d,fill,stroke='#526154',dash=''):
|
||||
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*13}" height="{d*13}" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
|
||||
def line(points,color,width):
|
||||
pts=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
|
||||
layout=json.loads((O/'daily-life-layout.json').read_text())
|
||||
line(layout['walking_loop'],'#adc49a',14)
|
||||
line([(14,1),(13,4),(13,7),(6,7)],'#adc49a',14)
|
||||
line([(-1,24),(-7,21),(-14,18)],'#adc49a',14)
|
||||
label(9,27.4,['SHADED WALKING LOOP'],12)
|
||||
line([(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)],'#c9bca1',52)
|
||||
rect(-1,-12.5,12,6,'#c9bca1',stroke='#c9bca1')
|
||||
line([(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],'#d5cdbb',12)
|
||||
line([(-3,5.4),(-3,9),(-11,9)],'#b5a88c',16)
|
||||
rect(0,0,12,6,'#ddc8a9');label(1,.7,['SHARED HOME','Kitchen · dining'],10)
|
||||
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['VERANDA'],11)
|
||||
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.8,1.5,['Short link'],10)
|
||||
rect(-11.8,-4.9,8,5,'#ddc8a9');label(-11.8,-4.5,['PARENTS','Bedroom + bath'],11)
|
||||
rect(-11,12,4.5,4.8,'#c9c1a5');label(-11,12.4,['QUIET','ROOM'],11)
|
||||
X,Y=pt(-11,12);p.append(f'<circle cx="{X}" cy="{Y+24}" r="5" fill="#626257"/>')
|
||||
for i,(x,y) in enumerate([(-3,17),(7,15)]):
|
||||
rect(x,y,4.5,4.8,'#e6e8de',stroke='#98a394',dash='stroke-dasharray="5 5"');label(x,y+.6,['FUTURE',f'SPACE {i+1}'],11)
|
||||
label(2,21.4,['Faint outlines only / build when needed'],12)
|
||||
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="29" ry="25" fill="#bc9d75" stroke="#526154" stroke-width="2"/>');label(-23,2,['SAUNA'],10)
|
||||
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7.5,['Outdoor shower'],11)
|
||||
for x in [-23.95,-26.25]:
|
||||
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="13" fill="#6b7f61"/>')
|
||||
label(-25,-10.5,['Rainwater tanks'],11)
|
||||
rect(-3,-19,8,7,'#bcb9ac');label(-3,-18.6,['GARAGE','8 × 7 m'],12)
|
||||
rect(-4.7,-19.1,1.9,4.35,'#788c83');rect(-.47,-21.5,2.5,.76,'#ab9673')
|
||||
label(-3,-24,['One car · workbench · bike racks'],12)
|
||||
label(20,-25,['4 m gravel access road'],13)
|
||||
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7.4,['GREENHOUSE'],9)
|
||||
for x in [23.8,27.5]:
|
||||
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
|
||||
label(25.5,7.5,['FOOD GARDEN'],12)
|
||||
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17.2,['GROUND SOLAR'],10)
|
||||
label(34,-42,['Site approach'],12)
|
||||
rect(*layout['training'],'#b9c8ad');label(-12.2,-16.8,['COVERED','TRAINING','4.8 × 5.4 m'],10)
|
||||
line([(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],'#d5cdbb',12)
|
||||
label(-15.8,-23.2,['Bar · rings · weights','Bench · movable mats'],11)
|
||||
rect(*layout['outdoor_table'],'#d9c19b');rect(10,3.5,3.2,1.03,'#ab8962')
|
||||
rect(10,4.48,2.95,.43,'#bda078');rect(10,2.52,2.95,.43,'#bda078')
|
||||
rect(6.5,4.2,1,1.5,'#c8d0b8');label(10,9,['OUTDOOR TABLE','10 places + tea counter'],11)
|
||||
rect(*layout['table_tennis'],'#ced8c4');rect(11,-8.5,1.525,2.74,'#608570')
|
||||
label(11,-5.9,['TABLE TENNIS'],10);label(11,-12.8,['Folding table / level deck'],10)
|
||||
line([(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],'#d5cdbb',12)
|
||||
rect(*layout['future_basketball'],'#eceee4',stroke='#81947c',dash='stroke-dasharray="7 7"')
|
||||
label(13,-35,['FUTURE','BASKETBALL','10 × 10 m reserve'],11)
|
||||
label(13,-43,['Unbuilt / separate from driveway'],11)
|
||||
rect(30,10.9,1.8,.45,'#a58a66');label(32,14,['QUIET BENCH'],11)
|
||||
rect(29.8,1.5,.78,2.05,'#a58a66');label(35,2.4,['HARVEST','BENCH'],11)
|
||||
label(4,-4.3,['Breakfast · pantry · freezer'],10)
|
||||
rect(-3.8,.1,2.2,.95,'#f4f1e9')
|
||||
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="70" y1="970" x2="200" y2="970" stroke="#263a34" stroke-width="4"/><text x="70" y="999" font-size="13">10 metres</text>','<line x1="60" y1="1040" x2="1340" y2="1040" stroke="#bdc3b6"/><text x="60" y="1070" font-size="15">The Blender presentation fades one facade of each building so you can see inside. Walls remain in the model.</text>','<text x="60" y="1096" font-size="13">Concept layout only. The road approach is illustrative; no surveyed property boundary or approved access is implied.</text></svg>'])
|
||||
(O/'retreat-garage-plan.svg').write_text(''.join(p))
|
||||
127
blender/scripts/enhance_kitchen.py
Normal file
127
blender/scripts/enhance_kitchen.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# Executed by add_daily_life.py inside the scene's construction context.
|
||||
# Replace the old simplified kitchen, retaining its position and the shared room.
|
||||
old_prefixes=('Simple timber kitchen cabinet','Flat cabinet pull','Continuous kitchen worktop','Stainless sink','Exposed kitchen tap','Tap spout','Induction cooktop','Open timber kitchen shelf','Pantry ceramic jar')
|
||||
for o in list(bpy.data.objects):
|
||||
if o.name.startswith(old_prefixes):bpy.data.objects.remove(o,do_unlink=True)
|
||||
# Pale marble: distorted fine mineral veins, soft honed reflections.
|
||||
marble=mat('Honed natural marble - ivory and warm grey veins',(.78,.76,.70),.26)
|
||||
ns=marble.node_tree.nodes;ls=marble.node_tree.links;bs=ns.get('Principled BSDF')
|
||||
coord=ns.new('ShaderNodeTexCoord');noise=ns.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=2.8;noise.inputs['Detail'].default_value=5
|
||||
ls.new(coord.outputs['Generated'],noise.inputs['Vector'])
|
||||
scale=ns.new('ShaderNodeVectorMath');scale.operation='SCALE';scale.inputs[3].default_value=.33;ls.new(noise.outputs['Color'],scale.inputs[0])
|
||||
add=ns.new('ShaderNodeVectorMath');add.operation='ADD';ls.new(coord.outputs['Generated'],add.inputs[0]);ls.new(scale.outputs[0],add.inputs[1])
|
||||
wave=ns.new('ShaderNodeTexWave');wave.wave_type='BANDS';wave.bands_direction='DIAGONAL';wave.inputs['Scale'].default_value=3.7;wave.inputs['Distortion'].default_value=6;wave.inputs['Detail'].default_value=4;ls.new(add.outputs[0],wave.inputs['Vector'])
|
||||
ramp=ns.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].position=.055;ramp.color_ramp.elements[0].color=(.20,.21,.20,1);ramp.color_ramp.elements[1].position=.14;ramp.color_ramp.elements[1].color=(.81,.79,.73,1)
|
||||
ls.new(wave.outputs['Color'],ramp.inputs[0]);ls.new(ramp.outputs[0],bs.inputs['Base Color'])
|
||||
fine=ns.new('ShaderNodeTexNoise');fine.inputs['Scale'].default_value=170;ls.new(coord.outputs['Generated'],fine.inputs['Vector']);bump=ns.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.10;bump.inputs['Distance'].default_value=.0005;ls.new(fine.outputs['Fac'],bump.inputs['Height']);ls.new(bump.outputs[0],bs.inputs['Normal'])
|
||||
copperpot=textured('Copper cookware - brushed warm patina',(.32,.10,.035),(.72,.34,.13),(75,75,8),.26,.0006);copperpot.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.94
|
||||
stainless=mat('Cookware food-contact stainless lining',(.55,.59,.61),.23,.96)
|
||||
ovenblack=mat('Oven and induction reflective smoked glass',(.009,.016,.019),.12,.25)
|
||||
mark=mat('Induction subtle etched markings',(.36,.42,.42),.45)
|
||||
display=mat('Appliance soft white display',(.72,.82,.75),.3);display.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.65,.85,.74,1);display.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=.6
|
||||
# Run of detailed cabinets, including a 600 mm oven bay.
|
||||
for i,(x,w) in enumerate([(-5.15,.72),(-4.32,.90),(-3.36,.96),(-2.38,.96),(-1.49,.78),(-.88,.42)]):
|
||||
box('Kitchen timber cabinet carcass',(x,-2.53,F+.45),(w,.64,.88),wood,.008)
|
||||
box('Kitchen recessed dark plinth',(x,-2.53,F+.075),(w-.06,.55,.13),steel,.006)
|
||||
if i==4:continue
|
||||
for row,h in enumerate([.29,.56,.79]):
|
||||
box('Kitchen flush timber drawer',(x,-2.194,F+h),(w-.018,.032,.22 if row<2 else .19),wood,.006)
|
||||
rod('Kitchen brushed drawer pull',(x-.14,-2.162,F+h+.065),(x+.14,-2.162,F+h+.065),.009,metal,sides=12)
|
||||
# Worktop parts leave a real sink opening at x=-4.32.
|
||||
for x,w in [(-5.15,.80),(-2.405,3.21)]:box('Kitchen honed marble rear worktop',(x,-2.52,F+.925),(w,.75,.045),marble,.008)
|
||||
for y,d in [(-2.80,.19),(-2.225,.16)]:box('Sink cutout marble edge',(-4.32,y,F+.925),(.86,d,.045),marble,.005)
|
||||
box('Kitchen marble low splashback',(-3.1,-2.905,F+1.09),(4.95,.025,.28),marble,.006)
|
||||
# Recessed sink basin with separate bottom and four walls.
|
||||
box('Sink recessed basin floor',(-4.32,-2.5,F+.73),(.66,.40,.025),stainless,.015)
|
||||
for x in [-4.66,-3.98]:box('Sink basin side',(x,-2.5,F+.825),(.025,.42,.20),stainless,.009)
|
||||
for y in [-2.715,-2.285]:box('Sink basin end',(-4.32,y,F+.825),(.70,.025,.20),stainless,.009)
|
||||
rod('Sink drain',(-4.32,-2.5,F+.744),(-4.32,-2.5,F+.750),.044,steel,sides=32)
|
||||
# Curved copper tap, exposed and replaceable.
|
||||
pts=[(-4.32,-2.8,F+.95),(-4.32,-2.8,F+1.26)]
|
||||
for i in range(17):
|
||||
a=pi-pi*i/16;pts.append((-4.32,-2.69+.11*cos(a),F+1.26+.11*sin(a)))
|
||||
for i,(a,b) in enumerate(zip(pts,pts[1:])):rod('Copper mixer tap '+str(i),a,b,.014,copperpot,sides=16)
|
||||
rod('Tap outlet',pts[-1],(-4.32,-2.58,F+1.20),.014,copperpot,sides=16)
|
||||
rod('Tap lever',(-4.20,-2.78,F+.95),(-4.20,-2.78,F+1.07),.01,stainless,sides=12)
|
||||
# Built-in oven, facing the living room (north).
|
||||
ox=-1.49
|
||||
box('OVEN - brushed steel surround',(ox,-2.177,F+.49),(.62,.04,.68),metal,.013)
|
||||
box('OVEN - insulated glass door',(ox,-2.144,F+.43),(.555,.032,.44),ovenblack,.015)
|
||||
box('Oven inner viewing panel',(ox,-2.125,F+.435),(.435,.006,.285),mat('Oven glass subtle interior',(.025,.035,.035),.18),.018)
|
||||
for z in [F+.37,F+.49]:box('Oven internal rack hint',(ox,-2.120,z),(.39,.004,.008),metal)
|
||||
rod('Oven tubular handle',(ox-.23,-2.085,F+.663),(ox+.23,-2.085,F+.663),.018,metal,sides=20)
|
||||
for x in [ox-.22,ox+.22]:rod('Oven handle standoff',(x,-2.14,F+.663),(x,-2.085,F+.663),.012,metal,sides=16)
|
||||
box('Oven control fascia',(ox,-2.142,F+.779),(.57,.028,.115),ovenblack,.007)
|
||||
for x in [ox-.205,ox+.205]:rod('Oven rotary dial',(x,-2.13,F+.78),(x,-2.097,F+.78),.027,metal,sides=32)
|
||||
box('Oven display',(ox,-2.121,F+.78),(.14,.004,.036),display,.002)
|
||||
for i in range(9):box('Oven bottom ventilation slot',(ox-.24+i*.06,-2.15,F+.17),(.035,.009,.011),black,.002)
|
||||
# Four-zone induction glass and etched circles.
|
||||
box('INDUCTION - four zone glass cooktop',(-1.49,-2.51,F+.961),(.70,.53,.018),ovenblack,.009)
|
||||
def horizontal_ring(name,x,y,z,r,t,material):
|
||||
ob=ring(name,(0,0,0),r,t,material);ob.rotation_euler.x=pi/2;ob.location=(x,y,z);return ob
|
||||
for x,y,r in [(-1.67,-2.64,.105),(-1.30,-2.64,.086),(-1.68,-2.40,.082),(-1.32,-2.40,.103)]:
|
||||
horizontal_ring('Induction etched cooking zone',x,y,F+.973,r,.0012,mark)
|
||||
for i in range(7):box('Induction touch slider',(-1.65+i*.05,-2.277,F+.972),(.019,.009,.002),mark,.001)
|
||||
# Slim extraction canopy with visible duct; no hidden construction language.
|
||||
box('Kitchen extraction hood',(-1.49,-2.49,F+1.68),(.82,.54,.075),metal,.01)
|
||||
for x in [-1.74,-1.24]:box('Hood warm task light',(x,-2.35,F+1.639),(.08,.12,.003),cream)
|
||||
rod('Exposed kitchen extraction duct',(-1.49,-2.60,F+1.71),(-1.49,-2.60,F+2.33),.105,metal,sides=48)
|
||||
# Timber shelving away from hob and exposed copper cookware rail.
|
||||
box('Kitchen upper hardwood shelf',(-3.96,-2.72,F+1.84),(2.77,.32,.05),wood,.008)
|
||||
rod('Copper cookware hanging rail',(-5.1,-2.57,F+1.52),(-2.75,-2.57,F+1.52),.014,copperpot,sides=20)
|
||||
for x in [-5.04,-2.81]:rod('Cookware rail wall bracket',(x,-2.89,F+1.52),(x,-2.57,F+1.52),.012,metal,sides=16)
|
||||
# Hollow copper vessels with stainless lining; induction-compatible base concept.
|
||||
def vessel(n,x,y,z,r,h):
|
||||
N=64;profile=[(0,0),(r*.83,0),(r,.025),(r,h),(r-.012,h+.003),(r-.016,.036),(0,.036)]
|
||||
vs=[(x+rr*cos(i*2*pi/N),y+rr*sin(i*2*pi/N),z+zz) for rr,zz in profile for i in range(N)]
|
||||
fs=[]
|
||||
for j in range(len(profile)-1):
|
||||
for i in range(N):fs.append((j*N+i,j*N+(i+1)%N,(j+1)*N+(i+1)%N,(j+1)*N+i))
|
||||
ob=mesh(n,vs,fs,copperpot);ob.data.materials.append(stainless)
|
||||
for p in ob.data.polygons:p.use_smooth=True;p.material_index=1 if p.index//N>=3 else 0
|
||||
rod(n+' induction compatible base concept',(x,y,z-.003),(x,y,z+.004),r*.85,stainless,sides=64)
|
||||
horizontal_ring(n+' rolled rim',x,y,z+h,r-.005,.006,stainless)
|
||||
return ob
|
||||
vessel('COPPER STOCKPOT',-2.91,-2.50,F+.954,.18,.235)
|
||||
for x in [-3.13,-2.69]:
|
||||
rod('Stockpot handle',(x,-2.57,F+1.12),(x,-2.43,F+1.12),.012,metal,sides=16)
|
||||
for y in [-2.57,-2.43]:rod('Stockpot handle fixing',(x,y,F+1.12),(x+(.055 if x<-2.9 else -.055),y,F+1.12),.009,metal,sides=12)
|
||||
vessel('COPPER SAUCEPAN',-1.32,-2.40,F+.977,.09,.09)
|
||||
rod('Saucepan handle',(-1.22,-2.40,F+1.05),(-.95,-2.40,F+1.09),.012,metal,sides=20)
|
||||
vessel('COPPER FRYING PAN',-3.45,.02,F+.965,.155,.037)
|
||||
rod('Frying pan long handle',(-3.29,.02,F+1.0),(-3.0,.02,F+1.03),.013,metal,sides=20)
|
||||
# Marble-topped island: central prep surface clear of the dining table.
|
||||
ix,iy=-3.8,.10
|
||||
box('ISLAND - warm timber cabinet body',(ix,iy,F+.44),(2.05,.83,.88),wood,.012)
|
||||
box('ISLAND - recessed plinth',(ix,iy,F+.06),(1.93,.73,.12),steel,.007)
|
||||
box('ISLAND - honed marble countertop',(ix,iy,F+.94),(2.2,.95,.06),marble,.012)
|
||||
for x in [ix-.68,ix,ix+.68]:
|
||||
for z in [.30,.69]:
|
||||
box('Island drawer front',(x,iy-.425,F+z),(.657,.028,.36),wood,.006)
|
||||
rod('Island drawer pull',(x-.14,iy-.448,F+z+.09),(x+.14,iy-.448,F+z+.09),.009,metal,sides=16)
|
||||
for i in range(29):box('Island north face timber slat',(ix-.98+i*.07,iy+.425,F+.49),(.042,.023,.75),woodz,.004)
|
||||
box('Island end grain chopping board',(-4.37,.14,F+.985),(.45,.34,.03),wood,.015)
|
||||
# Tea crockery and pantry objects add scale without filling the work surface.
|
||||
for x in [-5.03,-4.67,-4.30]:
|
||||
vessel('Shelf ceramic storage vessel',x,-2.71,F+1.87,.085,.18)
|
||||
for ob in list(col.objects):
|
||||
if ob.name.startswith('Shelf ceramic storage vessel') and ob.type=='MESH':
|
||||
for slot in range(len(ob.data.materials)):ob.data.materials[slot]=cream
|
||||
# Rebuild only the old furniture proxy so replaced kitchen blocks disappear in fast view.
|
||||
furniture=bpy.data.collections.get('10 Furniture');fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
|
||||
oldproxy=bpy.data.objects.get('FAST | 10 Furniture')
|
||||
if oldproxy and furniture:
|
||||
vs=[];fs=[];indices=[];materials=[];mapping={}
|
||||
for ob in furniture.objects:
|
||||
if ob.type!='MESH' or ob.hide_render:continue
|
||||
remap=[]
|
||||
for m in ob.data.materials:
|
||||
if m not in mapping:mapping[m]=len(materials);materials.append(m)
|
||||
remap.append(mapping[m])
|
||||
world=Matrix.LocRotScale(ob.location.copy(),ob.rotation_euler.to_quaternion(),ob.scale.copy());off=len(vs);vs.extend(tuple(world@v.co) for v in ob.data.vertices)
|
||||
for poly in ob.data.polygons:fs.append(tuple(off+j for j in poly.vertices));indices.append(remap[poly.material_index])
|
||||
me=bpy.data.meshes.new('Furniture after kitchen replacement');me.from_pydata(vs,[],fs);me.update()
|
||||
for m in materials:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',indices);oldproxy.data=me
|
||||
kitchen_checks={'oven':1,'induction_zones':4,'copper_stockpot':1,'copper_saucepan':1,'copper_frying_pan':1,'marble_island_m':[2.2,.95],'island_rear_work_aisle_m':1.67,'old_simplified_kitchen_removed':True,'fast_furniture_refreshed':bool(oldproxy)}
|
||||
s['Kitchen design']='Honed marble island, detailed timber drawers, oven, four-zone induction, extraction, hollow copper cookware with stainless food-contact lining and an induction-compatible base concept.'
|
||||
19
blender/scripts/verify_daily_life.py
Normal file
19
blender/scripts/verify_daily_life.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import bpy,json,os
|
||||
s=bpy.context.scene;out=os.path.dirname(__file__)
|
||||
chapters=json.load(open(out+'/tour-chapters-daily-life.json'))
|
||||
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
||||
issues=[];captions=[]
|
||||
for c in chapters:
|
||||
frame=c.get('start',c.get('start_frame',1))+30;s.frame_set(frame);bpy.context.view_layer.update()
|
||||
shown=[o.data.body for o in fonts if o.scale.length>.5]
|
||||
captions.append({'frame':frame,'titles':shown})
|
||||
if len(shown)!=1:issues.append({'frame':frame,'titles':shown})
|
||||
required=['OVEN - insulated glass door','INDUCTION - four zone glass cooktop','ISLAND - honed marble countertop','COPPER STOCKPOT','COPPER SAUCEPAN','COPPER FRYING PAN','PULL UP BAR - dedicated steel frame','OUTDOOR TABLE - ten place timber top','SHADED WALKING LOOP - garden and quiet room','HARVEST BENCH - garden sorting surface','PANTRY - tall timber cabinet','FREEZER - compact upright']
|
||||
missing=[n for n in required if n not in bpy.data.objects]
|
||||
report={'chapters':len(chapters),'end_frame':s.frame_end,'fps':s.render.fps,'missing':missing,'caption_issues':issues,'caption_samples':captions,'render_engine':s.render.engine,'render_size':[s.render.resolution_x,s.render.resolution_y],'detail_render_enabled':not bpy.data.collections['DETAIL - full geometry for renders'].hide_render,'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render,'training_steps':sum(o.name.startswith('Training access step') for o in s.objects),'play_steps':sum(o.name.startswith('Play deck access step') for o in s.objects)}
|
||||
json.dump(report,open(out+'/daily-life-verification.json','w'),indent=2)
|
||||
assert not missing and not issues,report
|
||||
assert report['chapters']==30 and s.frame_end==3600 and s.render.fps==24
|
||||
assert report['detail_render_enabled'] and report['fast_excluded_from_render']
|
||||
assert report['training_steps']==3 and report['play_steps']==3
|
||||
print('VERIFIED '+json.dumps({k:v for k,v in report.items() if k!='caption_samples'}),flush=True)
|
||||
Reference in New Issue
Block a user