feat: add socket-attach-points example (spawn mount contract)#118
Merged
Conversation
A spawn system bolting modules onto a prop at runtime rests on a chain of matrix identities that all fail quietly: a skipped matrix_parent_inverse, a basis built with the cross product the wrong way round, a transform frozen on the parent after the sockets were placed. Nothing in the gallery pinned that chain down — prop-origin-transform covers pivots and parent-inverse-orrery covers parent-inverse under animation, but neither asserts that a module dropped on a named socket with an identity local transform lands where the artist put it. The socket orientation check is deliberately non-circular: the mount pad mesh is swung onto its declared normal by a quaternion, while the socket basis is built by explicit Gram-Schmidt, and the check compares the socket's +Z against a Newell normal recomputed from raw vertex coordinates. Two independent derivations of the same declared value, so a basis-construction bug cannot hide behind the spec that produced it. Two hazards surfaced while authoring and are asserted rather than merely described, so they cannot change under us silently: applying a transform on an Empty root has no object data to bake into, so Blender pushes it down into every child (all 7 parent-inverses cleared, root scale handed to each child's local matrix) while preserving world matrices; and a child left selected during that apply gets the transform applied twice, drifting sockets 2.335 m. Signed-off-by: fOuttaMyPaint <tmhospitalitystrategies@gmail.com> Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
examples/socket-attach-points/— namedSKT_empties parented into an asset as spawn mounts, witnessed on a reusable survey drone chassis (Drone.Survey, 0.86 m span). Seven sockets (four 8°-canted rotor mounts, a belly camera mount, a dorsal mast mount, a rear mount tilted down), four interchangeable modules, 21 mesh parts, 32 material slots.The contract: a module parented to a socket with an identity local transform lands exactly where the artist authored it, oriented the way the naming convention promises. Every clause is a matrix identity and each one drifts silently.
prop-origin-transformcovers pivots andparent-inverse-orrerycovers parent-inverse under animation; neither asserts the socket→module seating contract. Sockets are authored in world space and then parented to a root posed at a non-trivial transform, somatrix_parent_inverseis load-bearing rather than incidentally identity.Contract and measured values (LIVE RUN)
det−1)min z)transform_applyworld preservationtransform_applyparent-inverse clearingNon-circularity. The pad mesh is swung onto its declared normal by
Vector((0,0,1)).rotation_difference(n); the socket basis is built by explicit Gram-Schmidt; the check compares against a Newell normal recomputed from raw vertex coordinates. Two independent derivations, so a basis bug cannot hide behind the spec that produced it.Falsification (LIVE RUN — every exit code proven to fail)
Probe harness:
.scratch/sap_falsify.py, each probe monkeypatches one thing and runs the realcheck().matrix_parent_inversez.cross(y)det == -1.000000CubeBaseline probe re-run after restoring: exit 0.
Version witness (LIVE RUN)
.scratch/blender-5.1.2-windows-x64/blender.exe→ reports 5.1.2.scratch/blender-4.5.11-windows-x64/blender.exe→ reports 4.5.11 LTSCheck output is byte-identical on both — every printed digit, plus identical framing numbers on the render path.
Hazards found while authoring
transform_applyon an Empty root pushes the transform down into the children. No object data to bake into, so all 7matrix_parent_inversevalues reset to identity and each child picks up the root scale locally (0.35 for an applied 1.35). World matrices survive; local ones do not. Asserted both ways.bm.normal_update()does not fix winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel, not an obvious hole.bmesh.ops.recalc_face_normalsis the fix;Part.finishnow always calls it.degenerate-bevel-weldcondition). 2.2 mm is clean.Gates
gallery_framing, enforced): fill 0.881x / 0.772y (band 0.70–0.90), margins L 0.066 / R 0.053 / B 0.122 / T 0.106 (floor 0.02),touch=none cross=none. The--falsifyrender takes a documented deviation — the modules leaving frame is the evidence.gallery_asset_quality, enforced): no default names; 32 materials on a 21-part hero, dominantSocketPad@0.33(ceiling 0.75);edge900.027 (ceiling 0.75); compactness 160.8 (informational).docs/gallery/contact-sheets/socket-attach-points-contact-sheet.webpvsarmature-bend,damped-track-aim,bmesh-gear. Mean luminance 69.0 (armature-bend 71.1, bmesh-gear 77.7, damped-track-aim 43.9 — inside range, not a fifth bright card); clip fraction 0.009% vs bmesh-gear's 0.134%. Stage darkness ✅, wedge warmth ✅ (contained pool, weakest axis), subject fill ✅, saturationdocs/gallery/asset-sheets/socket-attach-points.webpvscollision-hull-proxy,custom-normals-shade,vertex-weight-limit,lod-decimate-chain. Verdict: pass — the drone is the most part-dense and most material-varied object in the lineup, clearly not the least designed.teaches.split(".")precedent): cardaltand the witnesses callout carry the full strings — every dotted value (1.788e-07,1.794e-07,0.690128) survives untruncated. Em dashes are U+2014, correctly encoded.modular-kit-snapandlightmap-uv-channeland reads clearly distinct — cool dark grey against two warm scenes.Notes for reviewers (issues found in the gate tooling itself, not fixed here)
examples/gallery_asset_quality.pyraisesValueError: Vector.angle(other): zero length vectors have no valid angleon a mesh with a zero-area face rather than reporting it. It found my degenerate bevels, but via a crash.docs/VISUAL-STYLE.mdWebP recipe (img.save(filepath=..., quality=85)in Blender) does not apply the quality setting — it writes RGBA near-lossless. My hero came out 991 KB; the last two shipped heroes are 984 KB and 1.15 MB againstbmesh-gear's 33 KB. I encoded this example's assets with PIL at quality 85 instead (19 KB hero, 18 KB preview, pixel source unchanged from the PNG, no colour management re-applied). The two oversized existing heroes are left alone as out of scope.fullPagecapture rendered every card image blank on this 4516 px page even with images verified loaded (complete,naturalWidth 1280,opacity 1). The CLAUDE.md workaround ("full-page captures and crop locally") did not help; scroll-into-view plus a viewport capture did.featured_rank
Not assigned. The current featured set (
car-mirror-symmetry1 …modular-kit-snap8) is strong, and this card does not outclass a current member — it is a good asset but the least saturated of the group, which reads weakest at featured size.