4545sys .path .insert (0 , os .path .join (os .path .dirname (os .path .abspath (__file__ )), os .pardir ))
4646sys .dont_write_bytecode = True # keep examples/__pycache__ out of the repo tree
4747import gallery_framing
48+ import gallery_asset_quality
4849
4950TOL = 1e-6 # UV0 preservation tolerance
5051BOUNDS_TOL = 1e-5 # UV1 unit-square slack
@@ -88,11 +89,11 @@ def _box(name, dims, center, bevel=0.0):
8889 return me
8990
9091
91- def _wheel (name , radius , width , center ):
92+ def _wheel_disc (name , radius , width , center ):
9293 me = bpy .data .meshes .new (name )
9394 bm = bmesh .new ()
9495 try :
95- bmesh .ops .create_cone (bm , cap_ends = True , cap_tris = False , segments = 20 ,
96+ bmesh .ops .create_cone (bm , cap_ends = True , cap_tris = False , segments = 24 ,
9697 radius1 = radius , radius2 = radius , depth = width )
9798 # cone axis is Z; the wheel rolls around Y -> rotate verts, applied in data
9899 for v in bm .verts :
@@ -106,54 +107,179 @@ def _wheel(name, radius, width, center):
106107 return me
107108
108109
110+ def _ring (name , r_out , r_in , width , center , segments = 24 ):
111+ """Flat annulus (iron tire band) spun from a 4-vert profile, watertight."""
112+ me = bpy .data .meshes .new (name )
113+ bm = bmesh .new ()
114+ try :
115+ hw = width / 2
116+ prof = [(r_in , - hw ), (r_out , - hw ), (r_out , hw ), (r_in , hw )]
117+ vs = [bm .verts .new ((r , 0.0 , z )) for r , z in prof ]
118+ es = [bm .edges .new ((vs [i ], vs [(i + 1 ) % 4 ])) for i in range (4 )]
119+ bmesh .ops .spin (bm , geom = vs + es , cent = (0.0 , 0.0 , 0.0 ),
120+ axis = (0.0 , 0.0 , 1.0 ), dvec = (0.0 , 0.0 , 0.0 ),
121+ angle = 2 * math .pi , steps = segments , use_merge = True )
122+ for v in bm .verts :
123+ x , y , z = v .co .x , v .co .y , v .co .z
124+ v .co = (x + center [0 ], z + center [1 ], y + center [2 ])
125+ bm .to_mesh (me )
126+ finally :
127+ bm .free ()
128+ return me
129+
130+
131+ def _bolt_ring (name , count , ring_r , bolt_r , center ):
132+ """count bolt heads on a ring around Y — one mesh, disconnected islands."""
133+ me = bpy .data .meshes .new (name )
134+ bm = bmesh .new ()
135+ try :
136+ for i in range (count ):
137+ a = 2 * math .pi * i / count
138+ cx , cz = center [0 ] + ring_r * math .cos (a ), center [2 ] + ring_r * math .sin (a )
139+ before_v = set (bm .verts )
140+ bmesh .ops .create_uvsphere (bm , u_segments = 8 , v_segments = 6 , radius = bolt_r )
141+ for v in set (bm .verts ) - before_v :
142+ v .co .x += cx
143+ v .co .y += center [1 ]
144+ v .co .z += cz
145+ bm .to_mesh (me )
146+ finally :
147+ bm .free ()
148+ return me
149+
150+
151+ def _arc_band (bm , xc , width , radius , thickness , z0 , segments ):
152+ """One arched sheet spanning x in [xc, xc+width], solidified with rims."""
153+ sweep = math .radians (120.0 )
154+ a0 = math .radians (90.0 ) - sweep / 2
155+ outer , inner = [], []
156+ for i in range (segments + 1 ):
157+ a = a0 + sweep * i / segments
158+ ca , sa = math .cos (a ), math .sin (a )
159+ outer .append (bm .verts .new ((xc , radius * ca , z0 + radius * sa )))
160+ inner .append (bm .verts .new ((xc , (radius - thickness ) * ca ,
161+ z0 + (radius - thickness ) * sa )))
162+ def band (ring_o , ring_i ):
163+ for i in range (segments ):
164+ a , b = i , i + 1
165+ bm .faces .new ((ring_o [a ], ring_o [b ], ring_i [b ], ring_i [a ]))
166+ outer_x2 = [bm .verts .new ((v .co .x + width , v .co .y , v .co .z )) for v in outer ]
167+ inner_x2 = [bm .verts .new ((v .co .x + width , v .co .y , v .co .z )) for v in inner ]
168+ band (outer , inner ) # underside (thickness face)
169+ band (inner_x2 , outer_x2 ) # top face
170+ for i in range (segments ): # the two long rims
171+ a , b = i , i + 1
172+ bm .faces .new ((outer [a ], outer_x2 [a ], outer_x2 [b ], outer [b ]))
173+ bm .faces .new ((inner [a ], inner [b ], inner_x2 [b ], inner_x2 [a ]))
174+ bm .faces .new ((outer [0 ], inner [0 ], inner_x2 [0 ], outer_x2 [0 ]))
175+ bm .faces .new ((outer [- 1 ], outer_x2 [- 1 ], inner_x2 [- 1 ], inner [- 1 ]))
176+
177+
109178def _canopy (name , width , radius , thickness , z0 , segments = 14 ):
110179 """Arched canvas sheet: an arc band solidified with a rim, watertight."""
111180 me = bpy .data .meshes .new (name )
112181 bm = bmesh .new ()
113182 try :
114- sweep = math .radians (120.0 )
115- a0 = math .radians (90.0 ) - sweep / 2
116- outer , inner = [], []
117- for i in range (segments + 1 ):
118- a = a0 + sweep * i / segments
119- ca , sa = math .cos (a ), math .sin (a )
120- outer .append (bm .verts .new ((- width / 2 , radius * ca , z0 + radius * sa )))
121- inner .append (bm .verts .new ((- width / 2 , (radius - thickness ) * ca ,
122- z0 + (radius - thickness ) * sa )))
123- def band (off_x , ring_o , ring_i ):
124- for i in range (segments ):
125- a , b = i , i + 1
126- bm .faces .new ((ring_o [a ], ring_o [b ], ring_i [b ], ring_i [a ]))
127- outer_x2 = [bm .verts .new ((v .co .x + width , v .co .y , v .co .z )) for v in outer ]
128- inner_x2 = [bm .verts .new ((v .co .x + width , v .co .y , v .co .z )) for v in inner ]
129- band (0 , outer , inner ) # underside (thickness face)
130- band (width , inner_x2 , outer_x2 ) # top face
131- for i in range (segments ): # the two long rims
132- a , b = i , i + 1
133- bm .faces .new ((outer [a ], outer_x2 [a ], outer_x2 [b ], outer [b ]))
134- bm .faces .new ((inner [a ], inner [b ], inner_x2 [b ], inner_x2 [a ]))
135- for ring in ((outer , outer_x2 ), (inner , inner_x2 )): # end caps
136- for i in (0 , segments ):
137- pass
138- bm .faces .new ((outer [0 ], inner [0 ], inner_x2 [0 ], outer_x2 [0 ]))
139- bm .faces .new ((outer [- 1 ], outer_x2 [- 1 ], inner_x2 [- 1 ], inner [- 1 ]))
183+ _arc_band (bm , - width / 2 , width , radius , thickness , z0 , segments )
184+ bm .to_mesh (me )
185+ finally :
186+ bm .free ()
187+ return me
188+
189+
190+ def _canopy_ribs (name , positions , width , radius , thickness , z0 , segments = 14 ):
191+ """Thin wooden ribs following the canopy arc, one mesh, N islands."""
192+ me = bpy .data .meshes .new (name )
193+ bm = bmesh .new ()
194+ try :
195+ for px in positions :
196+ _arc_band (bm , px - width / 2 , width , radius , thickness , z0 , segments )
197+ bm .to_mesh (me )
198+ finally :
199+ bm .free ()
200+ return me
201+
202+
203+ def _bolts (name , positions , radius ):
204+ """Bolt head spheres at positions — one mesh, disconnected islands."""
205+ me = bpy .data .meshes .new (name )
206+ bm = bmesh .new ()
207+ try :
208+ for px , py , pz in positions :
209+ before_v = set (bm .verts )
210+ bmesh .ops .create_uvsphere (bm , u_segments = 8 , v_segments = 6 , radius = radius )
211+ for v in set (bm .verts ) - before_v :
212+ v .co .x += px
213+ v .co .y += py
214+ v .co .z += pz
215+ bm .to_mesh (me )
216+ finally :
217+ bm .free ()
218+ return me
219+
220+
221+ def _multi_box (name , specs ):
222+ """Several beveled boxes as one mesh (disconnected islands, each
223+ watertight). specs: (dims, center, bevel)."""
224+ me = bpy .data .meshes .new (name )
225+ bm = bmesh .new ()
226+ try :
227+ for dims , center , bevel in specs :
228+ before_e = set (bm .edges )
229+ before_v = set (bm .verts )
230+ bmesh .ops .create_cube (bm , size = 1.0 )
231+ new_edges = list (set (bm .edges ) - before_e )
232+ for v in set (bm .verts ) - before_v :
233+ v .co .x = v .co .x * dims [0 ] + center [0 ]
234+ v .co .y = v .co .y * dims [1 ] + center [1 ]
235+ v .co .z = v .co .z * dims [2 ] + center [2 ]
236+ if bevel > 0.0 :
237+ bmesh .ops .bevel (bm , geom = new_edges , offset = bevel , segments = 2 ,
238+ profile = 0.5 , affect = "EDGES" , clamp_overlap = True )
140239 bm .to_mesh (me )
141240 finally :
142241 bm .free ()
143242 return me
144243
145244
146245def build_cart_meshes ():
246+ """The cart as an assembly of named, watertight parts: real wheel
247+ assemblies (disc + hub + iron band + bolt ring), plank-grooved bed with
248+ side rails, corner brackets, and fasteners, and a ribbed canvas canopy."""
147249 parts = {}
148250 parts ["Bed" ] = _box ("Cart.Bed" , (2.2 , 1.2 , 0.22 ), (0.0 , 0.0 , 0.85 ), 0.03 )
149- parts ["Axle" ] = _wheel ("Cart.Axle" , 0.05 , 1.5 , (0.55 , 0.0 , 0.5 ))
150- parts ["Wheel.L" ] = _wheel ("Cart.Wheel.L" , 0.45 , 0.12 , (0.55 , 0.68 , 0.45 ))
151- parts ["Wheel.R" ] = _wheel ("Cart.Wheel.R" , 0.45 , 0.12 , (0.55 , - 0.68 , 0.45 ))
251+ parts ["Bed.Rails" ] = _multi_box ("Cart.Bed.Rails" , [
252+ ((2.2 , 0.06 , 0.18 ), (0.0 , 0.57 , 1.02 ), 0.015 ),
253+ ((2.2 , 0.06 , 0.18 ), (0.0 , - 0.57 , 1.02 ), 0.015 )])
254+ parts ["Bed.Grooves" ] = _multi_box ("Cart.Bed.Grooves" , [
255+ ((0.035 , 1.14 , 0.015 ), (x , 0.0 , 0.9575 ), 0.0 )
256+ for x in (- 0.66 , - 0.22 , 0.22 , 0.66 )])
257+ parts ["Bed.Brackets" ] = _multi_box ("Cart.Bed.Brackets" , [
258+ ((0.03 , 0.13 , 0.26 ), (sx * 1.105 , sy * 0.535 , 0.85 ), 0.008 )
259+ for sx in (- 1 , 1 ) for sy in (- 1 , 1 )])
260+ parts ["Bed.Bolts" ] = _bolts ("Cart.Bed.Bolts" , [
261+ (sx * 1.125 , sy * 0.535 , z ) for sx in (- 1 , 1 ) for sy in (- 1 , 1 )
262+ for z in (0.78 , 0.92 )], 0.018 )
263+ parts ["Axle" ] = _wheel_disc ("Cart.Axle" , 0.05 , 1.56 , (0.55 , 0.0 , 0.5 ))
264+ parts ["Axle.Caps" ] = _multi_box ("Cart.Axle.Caps" , [
265+ ((0.14 , 0.05 , 0.14 ), (0.55 , sy * 0.80 , 0.5 ), 0.02 ) for sy in (- 1 , 1 )])
266+ for tag , sy in (("L" , 1.0 ), ("R" , - 1.0 )):
267+ y = sy * 0.68
268+ parts [f"Wheel.{ tag } .Disc" ] = _wheel_disc (
269+ f"Cart.Wheel.{ tag } .Disc" , 0.45 , 0.10 , (0.55 , y , 0.45 ))
270+ parts [f"Wheel.{ tag } .Hub" ] = _wheel_disc (
271+ f"Cart.Wheel.{ tag } .Hub" , 0.14 , 0.16 , (0.55 , y , 0.45 ))
272+ parts [f"Wheel.{ tag } .Band" ] = _ring (
273+ f"Cart.Wheel.{ tag } .Band" , 0.47 , 0.435 , 0.10 , (0.55 , y , 0.45 ))
274+ parts [f"Wheel.{ tag } .Bolts" ] = _bolt_ring (
275+ f"Cart.Wheel.{ tag } .Bolts" , 6 , 0.09 , 0.02 , (0.55 , y + sy * 0.085 , 0.45 ))
152276 for tag , (px , py ) in (("FL" , (0.95 , 0.48 )), ("FR" , (0.95 , - 0.48 )),
153277 ("RL" , (- 0.95 , 0.48 )), ("RR" , (- 0.95 , - 0.48 ))):
154278 parts [f"Post.{ tag } " ] = _box (f"Cart.Post.{ tag } " , (0.09 , 0.09 , 1.35 ),
155279 (px , py , 1.55 ), 0.015 )
156280 parts ["Canopy" ] = _canopy ("Cart.Canopy" , 2.3 , 0.85 , 0.04 , 1.9 )
281+ parts ["Canopy.Ribs" ] = _canopy_ribs ("Cart.Canopy.Ribs" , (- 0.75 , 0.0 , 0.75 ),
282+ 0.07 , 0.875 , 0.035 , 1.9 )
157283 return parts
158284
159285
@@ -512,18 +638,30 @@ def render_still(path, engine, falsify=False):
512638 bpy .ops .wm .read_factory_settings (use_empty = True )
513639 sc = bpy .context .scene
514640
515- wood = make_material ("Wood" , (0.42 , 0.26 , 0.13 ), rough = 0.6 )
641+ wood = make_material ("Wood" , (0.38 , 0.22 , 0.10 ), rough = 0.55 )
516642 darkwood = make_material ("DarkWood" , (0.16 , 0.10 , 0.05 ), rough = 0.7 )
517- iron = make_material ("Iron" , (0.18 , 0.18 , 0.20 ), rough = 0.35 , metallic = 0.9 )
643+ iron = make_material ("Iron" , (0.26 , 0.26 , 0.30 ), rough = 0.3 , metallic = 0.9 )
518644 canvas = make_material ("Canvas" , (0.66 , 0.56 , 0.40 ), rough = 0.9 )
645+ groovemat = make_material ("Groove" , (0.05 , 0.04 , 0.03 ), rough = 0.9 )
646+
647+ mat_by_part = {
648+ "Bed" : wood , "Bed.Rails" : wood , "Bed.Grooves" : groovemat ,
649+ "Bed.Brackets" : iron , "Bed.Bolts" : iron ,
650+ "Axle" : iron , "Axle.Caps" : iron ,
651+ "Canopy" : canvas , "Canopy.Ribs" : darkwood ,
652+ }
653+ for tag in ("L" , "R" ):
654+ mat_by_part [f"Wheel.{ tag } .Disc" ] = darkwood
655+ mat_by_part [f"Wheel.{ tag } .Hub" ] = darkwood
656+ mat_by_part [f"Wheel.{ tag } .Band" ] = iron
657+ mat_by_part [f"Wheel.{ tag } .Bolts" ] = iron
658+ for tag in ("FL" , "FR" , "RL" , "RR" ):
659+ mat_by_part [f"Post.{ tag } " ] = darkwood
519660
520- mats = {"Bed" : wood , "Axle" : iron , "Wheel.L" : darkwood , "Wheel.R" : darkwood ,
521- "Canopy" : canvas }
522661 parts_obs = []
523662 meshes = build_cart_meshes ()
524663 for suffix , me in meshes .items ():
525- me .materials .append (mats .get (suffix .split ("." )[0 ], wood )
526- if not suffix .startswith ("Post" ) else wood )
664+ me .materials .append (mat_by_part [suffix ])
527665 me .uv_layers .new (name = LAYER0 )
528666 me .uv_layers .new (name = LAYER1 )
529667 write_uv0_box_projection (me )
@@ -553,10 +691,10 @@ def render_still(path, engine, falsify=False):
553691 # atlas board: the Bed's live UV1 as flat island polygons mapped onto the
554692 # board face — a change in the packed atlas moves the board geometry
555693 board_mat = make_material ("Board" , (0.04 , 0.04 , 0.05 ), rough = 0.5 , metallic = 0.4 )
556- board = _box ("Atlas.Board" , (0.07 , 1.7 , 1.7 ), (0.0 , 0.0 , 0.0 ), 0.02 )
694+ board = _box ("Atlas.Board" , (0.08 , 2.2 , 2.0 ), (0.0 , 0.0 , 0.0 ), 0.02 )
557695 board .materials .append (board_mat )
558696 board_ob = bpy .data .objects .new ("Atlas.Board" , board )
559- board_ob .location = (1.75 , 0.55 , 1.0 )
697+ board_ob .location = (2.05 , 0.55 , 1.15 )
560698 sc .collection .objects .link (board_ob )
561699
562700 bed = meshes ["Bed" ]
@@ -571,7 +709,7 @@ def render_still(path, engine, falsify=False):
571709 bm = bmesh .new ()
572710 try :
573711 # board face: +X side; UV [0,1]^2 -> y,z on the face (u -> -y, v -> z)
574- vs = [bm .verts .new ((0.0 , (0.5 - uv [0 ]) * 1.5 , (uv [1 ] - 0.5 ) * 1.5 ))
712+ vs = [bm .verts .new ((0.0 , (0.5 - uv [0 ]) * 1.9 , (uv [1 ] - 0.5 ) * 1.9 ))
575713 for uv in pts ]
576714 bm .faces .new (vs )
577715 bm .to_mesh (me )
@@ -582,7 +720,7 @@ def render_still(path, engine, falsify=False):
582720 me .materials .append (make_material (f"IslandMat{ fi } " , rgb , rough = 0.5 ,
583721 emit = rgb , estr = 1.6 if defect else 0.5 ))
584722 ob = bpy .data .objects .new (f"Atlas.Island.{ fi } " , me )
585- ob .location = (1.75 + 0.037 + (0.004 if fi in dragged else 0.0 ), 0.55 , 1.0 )
723+ ob .location = (2.05 + 0.045 + (0.004 if fi in dragged else 0.0 ), 0.55 , 1.15 )
586724 sc .collection .objects .link (ob )
587725 iso_obs .append (ob )
588726
@@ -591,10 +729,10 @@ def render_still(path, engine, falsify=False):
591729 cam_data = bpy .data .cameras .new ("Cam" )
592730 cam_data .lens = 47.0
593731 cam = bpy .data .objects .new ("Cam" , cam_data )
594- cam .location = (5.4 , - 6.6 , 2.7 )
732+ cam .location = (5.9 , - 7.2 , 2.8 )
595733 sc .collection .objects .link (cam )
596734 aim = bpy .data .objects .new ("Aim" , None )
597- aim .location = (0.75 , 0.1 , 1.08 )
735+ aim .location = (0.85 , 0.1 , 1.05 )
598736 sc .collection .objects .link (aim )
599737 tr = cam .constraints .new ("TRACK_TO" )
600738 tr .target = aim
@@ -625,6 +763,10 @@ def render_still(path, engine, falsify=False):
625763 )
626764 if fcode :
627765 return fcode
766+ aqcode = gallery_asset_quality .check_asset_quality (
767+ sc , cam , hero = hero , stage = [floor , wall ])
768+ if aqcode :
769+ return aqcode
628770 bpy .ops .render .render (write_still = True )
629771 if not (os .path .exists (path ) and os .path .getsize (path ) > 0 ):
630772 print ("ERROR: render produced no file" , file = sys .stderr )
0 commit comments