@@ -190,7 +190,7 @@ <h1>prop-origin-transform</h1>
190190< ul > < li > < strong > Stale < code > matrix_world</ code > .</ strong > After a location edit, < code > matrix_world</ code > is unchanged until < code > view_layer.update()</ code > (same half of the orrery contract).</ li > < li > < strong > Scale apply via data API.</ strong > Non-uniform scale baked into verts; < code > obj.scale == (1,1,1)</ code > exactly afterward.</ li > < li > < strong > Origin at base center.</ strong > Local bbox < code > min.z == 0</ code > , XY centered; world AABB delta across the bake is < strong > 0</ strong > (gate < code > 1e-5</ code > ).</ li > < li > < strong > Bare-parent trap.</ strong > Parenting the accessory without MPI jumps it (~0.43 m measured); setting < code > matrix_parent_inverse</ code > restores world location (err ~3e-8).</ li > </ ul >
191191< p > < strong > What each check catches on failure:</ strong > skipping scale apply leaves non-identity scale; skipping origin bake leaves < code > min.z ≈ -0.756</ code > ; skipping MPI leaves the accessory teleported.</ p >
192192< p > < strong > Version witness:</ strong > byte-identical numbers on Blender 4.5.11 LTS and 5.1.2.</ p >
193- < p > The render is a dual panel: left < strong > TRAP</ strong > (bare parent — flanged conduit teleports off an empty mount socket, hazard material ) vs right < strong > MPI KEEP</ strong > (flange seated in the socket), with emissive origin markers at each pedestal's base. Check closed forms are unchanged; the flange/socket are render form only .</ p >
193+ < p > The render is a dual panel: left < strong > TRAP</ strong > (bare parent — flanged conduit teleports off a deep shadowed mount socket) vs right < strong > MPI KEEP</ strong > (same metal fitting seated in the socket), with emissive origin markers at each pedestal's base. Accessory and pedestal stay in one material family so displacement — not color — carries the proof. Check closed forms are unchanged .</ p >
194194< h2 > Run</ h2 >
195195< pre > < code > blender --background --python prop_origin_transform.py --
196196blender --background --python prop_origin_transform.py -- --output origin.png
@@ -315,60 +315,65 @@ <h2>Source</h2>
315315
316316
317317< span class ="k "> def</ span > build_accessory_mesh():
318- < span class ="s "> """Flanged conduit stub — reads as a bolted part, not a stray cylinder ."""</ span >
318+ < span class ="s "> """Flanged conduit stub — short pipe + mounting flange + bolt heads ."""</ span >
319319 me = bpy.data.meshes.new(< span class ="s "> "Conduit"</ span > )
320320 bm = bmesh.new()
321321 < span class ="k "> try</ span > :
322- < span class ="c "> # Barrel along +Y (away from prop face when mounted)</ span >
323- bmesh.ops.create_cone(
324- bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 14 </ span > , radius1=< span class ="n "> 0.08 </ span > , radius2=< span class ="n "> 0.08</ span > , depth=< span class ="n "> 0.42 </ span > ,
322+ < span class ="c "> # Barrel along +Y (away from prop face when mounted); origin at flange face </ span >
323+ barrel = bmesh.ops.create_cone(
324+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 16 </ span > , radius1=< span class ="n "> 0.09 </ span > , radius2=< span class ="n "> 0.08</ span > , depth=< span class ="n "> 0.48 </ span > ,
325325 )
326326 bmesh.ops.rotate(
327- bm, verts=bm. verts, cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
327+ bm, verts=barrel[ < span class =" s " > " verts" </ span > ] , cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
328328 matrix=Matrix.Rotation(math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 4</ span > , < span class ="s "> "X"</ span > ),
329329 )
330- bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , -< span class ="n "> 0.21 </ span > , < span class ="n "> 0.0</ span > ), verts=bm. verts)
331- < span class ="c "> # Mounting flange at the prop-facing end </ span >
332- ret = bmesh.ops.create_cone(
333- bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 16 </ span > , radius1=< span class ="n "> 0.16 </ span > , radius2=< span class ="n "> 0.16 </ span > , depth=< span class ="n "> 0.04 </ span > ,
330+ bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , -< span class ="n "> 0.24 </ span > , < span class ="n "> 0.0</ span > ), verts=barrel[ < span class =" s " > " verts" </ span > ] )
331+ < span class ="c "> # Thick mounting flange at the prop-facing (+Y local before rotate → y≈0) </ span >
332+ flange = bmesh.ops.create_cone(
333+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 20 </ span > , radius1=< span class ="n "> 0.20 </ span > , radius2=< span class ="n "> 0.20 </ span > , depth=< span class ="n "> 0.06 </ span > ,
334334 )
335335 bmesh.ops.rotate(
336- bm, verts=ret [< span class ="s "> "verts"</ span > ], cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
336+ bm, verts=flange [< span class ="s "> "verts"</ span > ], cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
337337 matrix=Matrix.Rotation(math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 4</ span > , < span class ="s "> "X"</ span > ),
338338 )
339- bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , < span class ="n "> 0.02 </ span > , < span class ="n "> 0.0</ span > ), verts=ret [< span class ="s "> "verts"</ span > ])
340- < span class ="c "> # Four bolt heads on the flange</ span >
339+ bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , < span class ="n "> 0.03 </ span > , < span class ="n "> 0.0</ span > ), verts=flange [< span class ="s "> "verts"</ span > ])
340+ < span class ="c "> # Four bolt heads on the flange face (camera-visible when seated) </ span >
341341 < span class ="k "> for</ span > ang < span class ="k "> in</ span > (< span class ="n "> 45</ span > , < span class ="n "> 135</ span > , < span class ="n "> 225</ span > , < span class ="n "> 315</ span > ):
342342 rad = math.radians(ang)
343- bx, bz = < span class ="n "> 0.11 </ span > * math.cos(rad), < span class ="n "> 0.11 </ span > * math.sin(rad)
343+ bx, bz = < span class ="n "> 0.14 </ span > * math.cos(rad), < span class ="n "> 0.14 </ span > * math.sin(rad)
344344 br = bmesh.ops.create_cone(
345- bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 8</ span > , radius1=< span class ="n "> 0.025 </ span > , radius2=< span class ="n "> 0.025 </ span > , depth=< span class ="n "> 0.03 </ span > ,
345+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 8</ span > , radius1=< span class ="n "> 0.032 </ span > , radius2=< span class ="n "> 0.032 </ span > , depth=< span class ="n "> 0.04 </ span > ,
346346 )
347347 bmesh.ops.rotate(
348348 bm, verts=br[< span class ="s "> "verts"</ span > ], cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
349349 matrix=Matrix.Rotation(math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 4</ span > , < span class ="s "> "X"</ span > ),
350350 )
351- bmesh.ops.translate(bm, vec=(bx, < span class ="n "> 0.05 </ span > , bz), verts=br[< span class ="s "> "verts"</ span > ])
351+ bmesh.ops.translate(bm, vec=(bx, < span class ="n "> 0.07 </ span > , bz), verts=br[< span class ="s "> "verts"</ span > ])
352352 bm.to_mesh(me)
353353 < span class ="k "> finally</ span > :
354354 bm.free()
355355 < span class ="k "> return</ span > me
356356
357357
358358< span class ="k "> def</ span > build_socket_mesh():
359- < span class ="s "> """Empty mount collar left on the prop — the seat the accessory departed ."""</ span >
359+ < span class ="s "> """Deep mount well — shadowed interior reads as the vacated seat ."""</ span >
360360 me = bpy.data.meshes.new(< span class ="s "> "Socket"</ span > )
361361 bm = bmesh.new()
362362 < span class ="k "> try</ span > :
363- < span class ="c "> # Outer ring </ span >
363+ < span class ="c "> # Outer collar (matches flange OD) </ span >
364364 bmesh.ops.create_cone(
365- bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 20 </ span > , radius1=< span class ="n "> 0.18 </ span > , radius2=< span class ="n "> 0.18 </ span > , depth=< span class ="n "> 0.05 </ span > ,
365+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 24 </ span > , radius1=< span class ="n "> 0.24 </ span > , radius2=< span class ="n "> 0.24 </ span > , depth=< span class ="n "> 0.07 </ span > ,
366366 )
367- < span class ="c "> # Punch a visual recess by adding an inset darker disk slightly proud </ span >
368- inner = bmesh.ops.create_cone(
369- bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 16 </ span > , radius1=< span class ="n "> 0.09 </ span > , radius2=< span class ="n "> 0.09 </ span > , depth=< span class ="n "> 0.02 </ span > ,
367+ < span class ="c "> # Deep dark well — into the prop after rotate </ span >
368+ well = bmesh.ops.create_cone(
369+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 20 </ span > , radius1=< span class ="n "> 0.13 </ span > , radius2=< span class ="n "> 0.11 </ span > , depth=< span class ="n "> 0.18 </ span > ,
370370 )
371- bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.02</ span > ), verts=inner[< span class ="s "> "verts"</ span > ])
371+ bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , -< span class ="n "> 0.06</ span > ), verts=well[< span class ="s "> "verts"</ span > ])
372+ < span class ="c "> # Backstop disk (shadowed pocket floor)</ span >
373+ floor = bmesh.ops.create_cone(
374+ bm, cap_ends=< span class ="k "> True</ span > , segments=< span class ="n "> 16</ span > , radius1=< span class ="n "> 0.10</ span > , radius2=< span class ="n "> 0.10</ span > , depth=< span class ="n "> 0.025</ span > ,
375+ )
376+ bmesh.ops.translate(bm, vec=(< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , -< span class ="n "> 0.16</ span > ), verts=floor[< span class ="s "> "verts"</ span > ])
372377 bmesh.ops.rotate(
373378 bm, verts=bm.verts, cent=(< span class ="n "> 0</ span > , < span class ="n "> 0</ span > , < span class ="n "> 0</ span > ),
374379 matrix=Matrix.Rotation(math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 4</ span > , < span class ="s "> "X"</ span > ),
@@ -656,17 +661,13 @@ <h2>Source</h2>
656661 bpy.ops.wm.read_factory_settings(use_empty=< span class ="k "> True</ span > )
657662 sc = bpy.context.scene
658663
659- < span class ="c "> # Teal pedestal — distinct from mesh-hygiene brass valve</ span >
664+ < span class ="c "> # Teal pedestal — accessory shares metal fitting in the same family (no</ span >
665+ < span class ="c "> # primary-color emission blobs). Empty socket is a dark recessed well.</ span >
660666 body_m = make_material(< span class ="s "> "BodyR"</ span > , (< span class ="n "> 0.14</ span > , < span class ="n "> 0.48</ span > , < span class ="n "> 0.42</ span > ), rough=< span class ="n "> 0.4</ span > , metallic=< span class ="n "> 0.55</ span > )
661- acc_m = make_material(< span class ="s "> "AccR"</ span > , (< span class ="n "> 0.62</ span > , < span class ="n "> 0.48</ span > , < span class ="n "> 0.18</ span > ), rough=< span class ="n "> 0.35</ span > , metallic=< span class ="n "> 0.85</ span > )
662- trap_m = make_material(
663- < span class ="s "> "TrapAcc"</ span > , (< span class ="n "> 0.95</ span > , < span class ="n "> 0.28</ span > , < span class ="n "> 0.08</ span > ), rough=< span class ="n "> 0.4</ span > , metallic=< span class ="n "> 0.35</ span > ,
664- emit=(< span class ="n "> 1.0</ span > , < span class ="n "> 0.3</ span > , < span class ="n "> 0.05</ span > ), estr=< span class ="n "> 1.1</ span > ,
665- )
666- sock_m = make_material(< span class ="s "> "Socket"</ span > , (< span class ="n "> 0.12</ span > , < span class ="n "> 0.14</ span > , < span class ="n "> 0.15</ span > ), rough=< span class ="n "> 0.55</ span > , metallic=< span class ="n "> 0.4</ span > )
667+ acc_m = make_material(< span class ="s "> "AccR"</ span > , (< span class ="n "> 0.42</ span > , < span class ="n "> 0.44</ span > , < span class ="n "> 0.40</ span > ), rough=< span class ="n "> 0.32</ span > , metallic=< span class ="n "> 0.88</ span > )
668+ sock_m = make_material(< span class ="s "> "Socket"</ span > , (< span class ="n "> 0.06</ span > , < span class ="n "> 0.07</ span > , < span class ="n "> 0.08</ span > ), rough=< span class ="n "> 0.7</ span > , metallic=< span class ="n "> 0.2</ span > )
667669 sock_empty_m = make_material(
668- < span class ="s "> "SocketEmpty"</ span > , (< span class ="n "> 0.35</ span > , < span class ="n "> 0.55</ span > , < span class ="n "> 0.75</ span > ), rough=< span class ="n "> 0.45</ span > , metallic=< span class ="n "> 0.2</ span > ,
669- emit=(< span class ="n "> 0.3</ span > , < span class ="n "> 0.5</ span > , < span class ="n "> 0.75</ span > ), estr=< span class ="n "> 0.6</ span > ,
670+ < span class ="s "> "SocketEmpty"</ span > , (< span class ="n "> 0.015</ span > , < span class ="n "> 0.018</ span > , < span class ="n "> 0.02</ span > ), rough=< span class ="n "> 0.95</ span > , metallic=< span class ="n "> 0.0</ span > ,
670671 )
671672
672673 < span class ="k "> def</ span > make_prop(name, loc, rot_z=< span class ="n "> 0.0</ span > ):
@@ -709,8 +710,8 @@ <h2>Source</h2>
709710 bpy.context.view_layer.update()
710711 < span class ="k "> return</ span > ob
711712
712- left = make_prop(< span class ="s "> "TrapProp"</ span > , (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ), rot_z=math.radians(< span class ="n "> 25 </ span > ))
713- right = make_prop(< span class ="s "> "KeepProp"</ span > , (< span class ="n "> 2.0 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ), rot_z=math.radians(-< span class ="n "> 8 </ span > ))
713+ left = make_prop(< span class ="s "> "TrapProp"</ span > , (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ), rot_z=math.radians(< span class ="n "> 55 </ span > ))
714+ right = make_prop(< span class ="s "> "KeepProp"</ span > , (< span class ="n "> 2.2 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > ), rot_z=math.radians(-< span class ="n "> 10 </ span > ))
714715
715716 bpy.context.view_layer.update()
716717 left_front = left.matrix_world @ Vector((< span class ="n "> 0.0</ span > , -< span class ="n "> 0.52</ span > , < span class ="n "> 0.78</ span > ))
@@ -719,10 +720,10 @@ <h2>Source</h2>
719720 make_socket(< span class ="s "> "TrapSocket"</ span > , left_front, sock_empty_m, parent=left)
720721 make_socket(< span class ="s "> "KeepSocket"</ span > , right_front, sock_m, parent=right)
721722
722- trap_acc = make_acc(< span class ="s "> "TrapAcc"</ span > , left_front + Vector((< span class ="n "> 0.0</ span > , -< span class ="n "> 0.08 </ span > , < span class ="n "> 0.0</ span > )), trap_m )
723- keep_acc = make_acc(< span class ="s "> "KeepAcc"</ span > , right_front + Vector((< span class ="n "> 0.0</ span > , -< span class ="n "> 0.08 </ span > , < span class ="n "> 0.0</ span > )), acc_m)
724- trap_acc.scale = (< span class ="n "> 1.35 </ span > , < span class ="n "> 1.35 </ span > , < span class ="n "> 1.35 </ span > )
725- keep_acc.scale = (< span class ="n "> 1.35 </ span > , < span class ="n "> 1.35 </ span > , < span class ="n "> 1.35 </ span > )
723+ trap_acc = make_acc(< span class ="s "> "TrapAcc"</ span > , left_front + Vector((< span class ="n "> 0.0</ span > , -< span class ="n "> 0.05 </ span > , < span class ="n "> 0.0</ span > )), acc_m )
724+ keep_acc = make_acc(< span class ="s "> "KeepAcc"</ span > , right_front + Vector((< span class ="n "> 0.0</ span > , -< span class ="n "> 0.05 </ span > , < span class ="n "> 0.0</ span > )), acc_m)
725+ trap_acc.scale = (< span class ="n "> 1.55 </ span > , < span class ="n "> 1.55 </ span > , < span class ="n "> 1.55 </ span > )
726+ keep_acc.scale = (< span class ="n "> 1.55 </ span > , < span class ="n "> 1.55 </ span > , < span class ="n "> 1.55 </ span > )
726727 bpy.context.view_layer.update()
727728
728729 w_before = trap_acc.matrix_world.translation.copy()
@@ -736,28 +737,30 @@ <h2>Source</h2>
736737 bpy.context.view_layer.update()
737738 print(
738739 < span class ="s "> f"</ span > < span class ="s "> render_trap_jump=</ span > {jumped:< span class ="s "> .4f</ span > }< span class ="s "> </ span > < span class ="s "> "</ span >
739- < span class ="s "> f"</ span > < span class ="s "> acc_at=</ span > {tuple(round(c, < span class ="n "> 3</ span > ) < span class ="k "> for</ span > c < span class ="k "> in</ span > trap_acc.matrix_world.translation)}< span class ="s "> "</ span >
740+ < span class ="s "> f"</ span > < span class ="s "> acc_at=</ span > {tuple(round(c, < span class ="n "> 3</ span > ) < span class ="k "> for</ span > c < span class ="k "> in</ span > trap_acc.matrix_world.translation)}< span class ="s "> </ span > < span class ="s "> "</ span >
741+ < span class ="s "> f"</ span > < span class ="s "> sock_at=</ span > {tuple(round(c, < span class ="n "> 3</ span > ) < span class ="k "> for</ span > c < span class ="k "> in</ span > left_front)}< span class ="s "> </ span > < span class ="s "> "</ span >
742+ < span class ="s "> f"</ span > < span class ="s "> sep=</ span > {(trap_acc.matrix_world.translation - left_front).length:< span class ="s "> .4f</ span > }< span class ="s "> "</ span >
740743 )
741744
742745 parent_keep_world(keep_acc, right)
743746 bpy.context.view_layer.update()
744747
745- < span class ="c "> # Origin markers at base — small, so they don't compete with the accessory</ span >
746748 origin_marker(sc, Vector(left.matrix_world.translation))
747749 origin_marker(sc, Vector(right.matrix_world.translation))
748750
749- placard(sc, < span class ="s "> "TRAP"</ span > , (< span class ="n "> 0.0 </ span > , -< span class ="n "> 1.05 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.16 </ span > )
750- placard(sc, < span class ="s "> "MPI KEEP"</ span > , (< span class ="n "> 2.0 </ span > , -< span class ="n "> 1.05 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.16 </ span > )
751+ placard(sc, < span class ="s "> "TRAP"</ span > , (< span class ="n "> 0.15 </ span > , -< span class ="n "> 1.25 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.11 </ span > )
752+ placard(sc, < span class ="s "> "MPI KEEP"</ span > , (< span class ="n "> 2.1 </ span > , -< span class ="n "> 1.25 </ span > , < span class ="n "> 0.02</ span > ), size=< span class ="n "> 0.11 </ span > )
751753
752754 build_studio(sc)
753755
754756 cam_data = bpy.data.cameras.new(< span class ="s "> "Cam"</ span > )
755- cam_data.lens = < span class ="n "> 42 .0</ span >
757+ cam_data.lens = < span class ="n "> 48 .0</ span >
756758 cam = bpy.data.objects.new(< span class ="s "> "Cam"</ span > , cam_data)
757- cam.location = (< span class ="n "> 0.95</ span > , -< span class ="n "> 5.6</ span > , < span class ="n "> 1.55</ span > )
759+ < span class ="c "> # Closer for fill; placards still clear the bottom</ span >
760+ cam.location = (< span class ="n "> 1.15</ span > , -< span class ="n "> 5.35</ span > , < span class ="n "> 1.45</ span > )
758761 sc.collection.objects.link(cam)
759762 aim = bpy.data.objects.new(< span class ="s "> "Aim"</ span > , < span class ="k "> None</ span > )
760- aim.location = (< span class ="n "> 0.95 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.72 </ span > )
763+ aim.location = (< span class ="n "> 1.1 </ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.70 </ span > )
761764 sc.collection.objects.link(aim)
762765 tr = cam.constraints.new(< span class ="s "> "TRACK_TO"</ span > )
763766 tr.target = aim
0 commit comments