Files
samford-valley-home/source/samford-retreat-garage/add_daily_life.py

219 lines
16 KiB
Python

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)