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)