139 lines
12 KiB
Python
139 lines
12 KiB
Python
import bpy,math,os,json
|
|
from mathutils import Vector,Matrix
|
|
from math import cos,sin,pi
|
|
O=os.path.dirname(__file__);s=bpy.context.scene;detail=bpy.data.collections['DETAIL - full geometry for renders'];fast=bpy.data.collections['00 FAST TOUR - lightweight viewport'];services=bpy.data.collections['05 Water and services']
|
|
c=bpy.data.collections.new('35 Shipping container - rainwater storage');detail.children.link(c);COL={'new':c};active='new'
|
|
src=open(O+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
|
|
cubefs=[tuple(reversed(f)) for f in cubefs]
|
|
green=textured('Container olive green powder-coated steel',(.065,.12,.080),(.095,.17,.11),(95,95,95),.43,.0006);green.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.65
|
|
steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];metal=bpy.data.materials['Appliance brushed metal'];wood=bpy.data.materials['Unpainted hardwood - long X grain'];gravel=bpy.data.materials['Loose local gravel - fine stones']
|
|
watermat=mat('Container enclosed water tanks pale grey polymer',(.59,.66,.57),.50);pipe=mat('Rainwater pipe dark polyethylene',(.03,.047,.035),.43);labelmat=mat('Container cream stencil lettering',(.74,.75,.62),.65)
|
|
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
|
removed=[]
|
|
for ob in list(services.objects):
|
|
if ob.name.startswith(('Gravel tank pad','2000 mm dark green poly rainwater tank','Poly tank','Tank service lid','Accessible tank outlet')):removed.append(ob.name);bpy.data.objects.remove(ob,do_unlink=True)
|
|
cx,cy,base=-25,-8,.42;L,W,H=6.06,2.44,2.59;xs,xe=cx-L/2,cx+L/2;ys,yn=cy-W/2,cy+W/2
|
|
# Rectangular container corner posts, ISO-style castings and structural rails.
|
|
box('CONTAINER WATER - steel base frame',(cx,cy,base+.075),(L,W,.15),green,.01)
|
|
box('Container non-absorbent interior floor',(cx,cy,base+.165),(L-.15,W-.15,.03),steel,.005)
|
|
for x in [xs+.08,xe-.08]:
|
|
for y in [ys+.08,yn-.08]:
|
|
g=ground(x,y);box('Container support pier',(x,y,(g+base)/2),(.38,.38,max(.1,base-g)),stone,.012)
|
|
box('Container structural corner post',(x,y,base+H/2),(.15,.15,H),green,.01)
|
|
for z in [base+.10,base+H-.10]:
|
|
box('ISO corner casting',(x,y,z),(.19,.18,.19),metal,.015)
|
|
box('ISO casting lifting recess',(x,y+(.093 if y>cy else -.093),z),(.075,.006,.065),steel,.014)
|
|
for y in [ys+.06,yn-.06]:
|
|
for z in [base+.18,base+H-.075]:box('Container long structural rail',(cx,y,z),(L,.13,.15),green,.007)
|
|
for x in [xs+.06,xe-.06]:box('Container upper end rail',(x,cy,base+H-.075),(.13,W,.15),green,.007)
|
|
# Corrugated long walls: folded sheet geometry rather than flat paint stripes.
|
|
for y,sign in [(ys,-1),(yn,1)]:
|
|
vs=[];faces=[];n=160
|
|
for i in range(n+1):
|
|
x=xs+.15+(L-.30)*i/n;fold=.028 if i%4 in [1,2] else -.028
|
|
vs.extend([(x,y+sign*fold,base+.24),(x,y+sign*fold,base+H-.15)])
|
|
for i in range(n):k=i*2;faces.append((k,k+2,k+3,k+1) if sign<0 else (k,k+1,k+3,k+2))
|
|
mesh('Container corrugated '+('south' if sign<0 else 'north')+' steel wall',vs,faces,green)
|
|
box('Container rain-shedding roof',(cx,cy,base+H-.035),(L-.12,W-.12,.055),green,.008)
|
|
for i in range(28):box('Container roof corrugation',(xs+.18+i*.21,cy,base+H+.005),(.055,W-.22,.025),green,.006)
|
|
# Recognisable double cargo doors at the east end, seals and locking bars.
|
|
for y in [cy-.56,cy+.56]:
|
|
box('Container cargo door dark gasket',(xe-.055,y,base+1.38),(.045,1.115,2.25),steel,.006)
|
|
box('Container cargo door panel',(xe-.021,y,base+1.38),(.04,1.065,2.20),green,.007)
|
|
for yy in [y-.25,y+.25]:
|
|
rod('Container door locking bar',(xe+.036,yy,base+.30),(xe+.036,yy,base+2.42),.015,metal,sides=16)
|
|
for z in [base+.42,base+1.19,base+2.28]:box('Door locking bar keeper',(xe+.051,yy,z),(.035,.075,.075),green,.007)
|
|
rod('Cargo door latch handle',(xe+.085,yy,base+1.13),(xe+.085,yy+.16,base+1.13),.013,metal,sides=12)
|
|
for z in [base+.48,base+1.36,base+2.21]:box('Container door hinge',(xe+.016,y+(.52 if y>cy else -.52),z),(.07,.12,.095),metal,.008)
|
|
# West end is a presentation cutaway, matching the other building cutaway views.
|
|
cut=mat('VIEW container west end transparent steel',(.07,.13,.085),.45,.6);ns=cut.node_tree.nodes;ls=cut.node_tree.links;tr=ns.new('ShaderNodeBsdfTransparent');mix=ns.new('ShaderNodeMixShader');mix.inputs[0].default_value=.07;ls.new(tr.outputs[0],mix.inputs[1]);ls.new(ns.get('Principled BSDF').outputs[0],mix.inputs[2]);ls.new(mix.outputs[0],ns.get('Material Output').inputs['Surface'])
|
|
cut.diffuse_color=(.07,.13,.085,.07)
|
|
ob=box('CONTAINER - west inspection cutaway',(xs+.035,cy,base+H/2),(.04,W-.20,H-.30),cut);ob.hide_set(True);ob['Presentation only']='West wall transparent for inspection. Restore olive container steel material for a closed exterior.'
|
|
# Two separate sealed tank vessels; the container shell itself is not a pressure/water vessel.
|
|
for x in [cx-1.55,cx+.75]:
|
|
z=base+.20
|
|
rod('INTERNAL RAINWATER TANK - sealed vessel',(x,cy,z),(x,cy,z+1.88),.92,watermat,sides=64)
|
|
rod('Internal water tank lid',(x,cy,z+1.88),(x,cy,z+1.92),.89,watermat,sides=64)
|
|
rod('Tank inspection hatch',(x,cy,z+1.92),(x,cy,z+1.965),.17,steel,sides=32)
|
|
for j in range(9):
|
|
zz=z+.15+j*.19;rod('Tank internal reinforcing band',(x,cy,zz),(x,cy,zz+.02),.933,watermat,sides=64)
|
|
rod('Tank top inlet connection',(x,cy,z+1.96),(x,cy,z+2.09),.033,pipe,sides=20)
|
|
rod('Tank manifold branch',(x,cy,z+2.09),(x,cy+.63,z+2.09),.033,pipe,sides=20)
|
|
rod('Tank low service connection',(x,cy-.88,z+.13),(x,cy-1.02,z+.13),.031,pipe,sides=20)
|
|
rod('Rainwater shared inlet manifold',(cx-1.55,cy+.63,base+2.29),(xe-.6,cy+.63,base+2.29),.035,pipe,sides=20)
|
|
rod('Rainwater shared outlet manifold',(cx-1.55,cy-1.02,base+.33),(xe-.6,cy-1.02,base+.33),.035,pipe,sides=20)
|
|
rod('Rainwater external inlet riser',(xe-.6,yn+.2,base+.1),(xe-.6,yn+.2,base+2.29),.04,pipe,sides=24)
|
|
rod('First flush collection chamber',(xe-1.0,yn+.2,base+.25),(xe-1.0,yn+.2,base+1.15),.10,watermat,sides=32)
|
|
box('Water service pump plinth',(xe-.7,ys-.65,base+.10),(.64,.72,.20),stone,.01)
|
|
rod('Water service pump motor',(xe-.7,ys-.89,base+.35),(xe-.7,ys-.41,base+.35),.13,metal,sides=32)
|
|
for x in [xe-.95,xe-.45]:box('Water pump fixing foot',(x,ys-.65,base+.245),(.075,.30,.09),steel,.007)
|
|
box('Rainwater outlet isolation valve',(xe-.6,ys-.25,base+.35),(.10,.11,.12),metal,.009)
|
|
box('Rainwater valve lever',(xe-.6,ys-.25,base+.46),(.21,.026,.02),green,.004)
|
|
rod('Inlet through-wall connection',(xe-.6,cy+.63,base+2.29),(xe-.6,yn+.2,base+2.29),.035,pipe,sides=20)
|
|
rod('Outlet through-wall connection',(xe-.6,cy-1.02,base+.33),(xe-.6,ys-.4,base+.33),.035,pipe,sides=20)
|
|
# Collection route toward the existing parents-wing downpipe, clear of paths above.
|
|
for a,b in [((-15.7,-7.83,-.15),(-18.4,-8.0,-.15)),((-18.4,-8,-.15),(xe-.6,yn+.2,-.15))]:rod('Buried roof collection pipe - concept route',a,b,.04,pipe,sides=20)
|
|
# Long-side stencil helps the object read clearly as a service container.
|
|
cu=bpy.data.curves.new('Rainwater container stencil','FONT');cu.body='RAINWATER';cu.size=.19;cu.extrude=.0005;cu.materials.append(labelmat)
|
|
t=bpy.data.objects.new('Container RAINWATER stencil',cu);c.objects.link(t);t.location=(cx+.7,yn+.035,base+1.5);t.rotation_euler=(pi/2,0,pi)
|
|
# Adjust the former tank path to approach the container's east cargo doors.
|
|
gardens=bpy.data.collections['34 Garden court - aligned pavilions and flowing landscape'];old=bpy.data.objects.get('Rainwater service garden path')
|
|
if old:bpy.data.objects.remove(old,do_unlink=True)
|
|
layout=json.load(open(O+'/garden-flow-layout.json'));layout['version']=5;layout['water_container']=[cx,cy,L,W,H]
|
|
points=[(-23,8.1),(-28.5,8.2),(-29.3,1),(-29.1,-5),(-29.1,-11.5),(-25,-12),(-20.5,-11.4),(-20.5,-8),(xe,-8)]
|
|
# Smooth Catmull-Rom route follows terrain and stops at the east cargo doors.
|
|
pts=[];p=[Vector(q) for q in points]
|
|
for i in range(len(p)-1):
|
|
a=p[max(0,i-1)];b=p[i];cc=p[i+1];d=p[min(i+2,len(p)-1)]
|
|
for j in range(20):
|
|
f=j/20;v=.5*(2*b+(-a+cc)*f+(2*a-5*b+4*cc-d)*f*f+(-a+3*b-3*cc+d)*f*f*f);pts.append(tuple(v))
|
|
pts.append(tuple(p[-1]));vs=[];fs=[]
|
|
for i,q in enumerate(pts):
|
|
v=Vector(q);d=Vector(pts[min(i+1,len(pts)-1)])-Vector(pts[max(i-1,0)]);side=Vector((-d.y,d.x)).normalized()*.575
|
|
for vv in [v-side,v+side]:vs.append((vv.x,vv.y,ground(*vv)+.085))
|
|
for i in range(len(pts)-1):k=i*2;fs.append((k,k+1,k+3,k+2))
|
|
mesh('Rainwater container service garden path',vs,fs,gravel)
|
|
for path in layout['paths']:
|
|
if path['name']=='Rainwater service garden path':path.update(name='Rainwater container service garden path',points=pts)
|
|
# Refresh only the affected fast display meshes, leaving tree simplification untouched.
|
|
def combine(coll,name):
|
|
old=bpy.data.objects.get(name)
|
|
if old:bpy.data.objects.remove(old,do_unlink=True)
|
|
vs=[];fs=[];indices=[];mats=[];lookup={}
|
|
for ob in coll.objects:
|
|
if ob.type!='MESH' or ob.hide_render or ob.hide_get():continue
|
|
remap=[]
|
|
for m in ob.data.materials:
|
|
if m not in lookup:lookup[m]=len(mats);mats.append(m)
|
|
remap.append(lookup[m])
|
|
mw=Matrix.LocRotScale(ob.location,ob.rotation_euler.to_quaternion(),ob.scale);off=len(vs);vs.extend(tuple(mw@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(name);me.from_pydata(vs,[],fs);me.update()
|
|
for m in mats:me.materials.append(m)
|
|
me.polygons.foreach_set('material_index',indices);ob=bpy.data.objects.new(name,me);fast.objects.link(ob)
|
|
combine(services,'FAST | 05 Water and services');combine(gardens,'FAST | Aligned garden court and flowers');combine(c,'FAST | Container rainwater storage')
|
|
cam=bpy.data.objects['PLAY TOUR camera'];chapters=json.load(open(O+'/tour-chapters-garden-flow.json'));chapters[12]['title']='13 / CONTAINER RAINWATER STORAGE'
|
|
for f,pos in [(1441,(-17,-2.5,4.6)),(1560,(-17.5,-3.1,4.3))]:
|
|
s.frame_set(f);cam.location=pos;cam.rotation_euler=(Vector((-25,-8,1.6))-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=40;cam.data.keyframe_insert('lens',frame=f)
|
|
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
|
for o in fonts:
|
|
if o.data.body.startswith('13 /'):o.data.body=chapters[12]['title'];half=36/40;o.location=(-half*.91,half*9/16*.84,-2)
|
|
for m in s.timeline_markers:
|
|
if m.frame==1441:m.name=chapters[12]['title']
|
|
s['Water storage']='20-foot container-style enclosure with two separate sealed internal tanks, corrugated steel shell, cargo doors, plumbing and a west inspection cutaway. Container shell is not modelled as the water-retaining vessel. Structural/loading, tank product and water-system sizing remain concept design.'
|
|
s['Current design']=s.get('Current design','')+' Exposed cylindrical tanks replaced by a container water-storage module in version 005.'
|
|
s.frame_set(1441);s.render.resolution_percentage=100
|
|
json.dump(layout,open(O+'/container-water-layout.json','w'),indent=2);json.dump(chapters,open(O+'/tour-chapters-container-water.json','w'),indent=2)
|
|
report={'container_dimensions_m':[L,W,H],'internal_tanks':sum(o.name.startswith('INTERNAL RAINWATER TANK') for o in s.objects),'old_exposed_tanks_removed':not any(o.name.startswith('2000 mm dark green poly rainwater tank') for o in s.objects),'west_inspection_cutaway':True,'tour_chapters':len(chapters),'removed_old_objects':len(removed)}
|
|
assert report['internal_tanks']==2 and report['old_exposed_tanks_removed']
|
|
json.dump(report,open(O+'/container-water-checks.json','w'),indent=2)
|
|
bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-solarpunk-container-water-tour.blend')
|
|
for o in fonts:o.hide_render=True
|
|
s.render.resolution_percentage=60;s.cycles.samples=32;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 name,f in [('container-water.png',1480),('container-water-overview.png',45)]:s.frame_set(f);s.render.filepath=O+'/'+name;bpy.ops.render.render(write_still=True)
|
|
cam.animation_data_clear();cam.data.animation_data_clear();cam.location=(-33,-3.5,4);cam.rotation_euler=(Vector((-25.5,-8,1.6))-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.lens=38;s.render.filepath=O+'/container-water-cutaway.png';bpy.ops.render.render(write_still=True)
|
|
print('CONTAINER_WATER_READY '+json.dumps(report),flush=True)
|