Skip to content

feat: add socket-attach-points example (spawn mount contract)#118

Merged
TMHSDigital merged 1 commit into
mainfrom
feat/socket-attach-points
Jul 24, 2026
Merged

feat: add socket-attach-points example (spawn mount contract)#118
TMHSDigital merged 1 commit into
mainfrom
feat/socket-attach-points

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

What this adds

examples/socket-attach-points/ — named SKT_ 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-transform covers pivots and parent-inverse-orrery covers 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, so matrix_parent_inverse is load-bearing rather than incidentally identity.

Contract and measured values (LIVE RUN)

Check Exit Measured Gate
Socket world matrix == authored 3 1.788e-07 1e-6
Basis orthonormal (Gram) / right-handed (det−1) 3 3.576e-07 / 2.980e-07 1e-6
Socket +Z == pad Newell normal 4 1.794e-07 1e-6
Socket origin == mount-face centroid 4 6.687e-08 1e-6
Up-axis fallback rule 4 1.943e-07 1e-6
Module seating offset / axis dot−1 5 0.0 / 1.735e-07 1e-6
Reuse hygiene (14 parts, min z) 6 0.00e+00 1e-4
Rigid invariance under re-pose 7 2.384e-07 1e-6
transform_apply world preservation 8 2.235e-07 1e-6
transform_apply parent-inverse clearing 8 7/7 cleared, child scale 0.35 for applied 1.35 asserted

Non-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 real check().

Probe Exit Measured error
sockets parented without matrix_parent_inverse 3 worst jump 0.690128 m
basis built with z.cross(y) 3 det == -1.000000
one rotor pad built flat instead of canted 4 1.395e-01 off the Newell normal
11° roll applied to the basis 4 1.917e-01 off the up rule
camera module nudged 4 mm 5 4.000e-03 m + non-identity local
hull datablock renamed Cube 6 default-name + namespace violation
one socket left unparented 7 1.417e+00 under the re-pose
children left selected during the apply 8 1.252e+00 – 2.244e+00 socket drift

Baseline 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 LTS

Check output is byte-identical on both — every printed digit, plus identical framing numbers on the render path.

Hazards found while authoring

  1. transform_apply on an Empty root pushes the transform down into the children. No object data to bake into, so all 7 matrix_parent_inverse values 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.
  2. Only the root may be selected during the apply — a selected child double-applies, 2.335 m drift.
  3. 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_normals is the fix; Part.finish now always calls it.
  4. Bevel offset near half the local edge length collapses — skid rails at 5 mm on a 9.9 mm octagon edge gave 3 zero-area faces each (the degenerate-bevel-weld condition). 2.2 mm is clean.

Gates

  • Framing (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 --falsify render takes a documented deviation — the modules leaving frame is the evidence.
  • Asset floors (gallery_asset_quality, enforced): no default names; 32 materials on a 21-part hero, dominant SocketPad@0.33 (ceiling 0.75); edge90 0.027 (ceiling 0.75); compactness 160.8 (informational).
  • Contact sheet: docs/gallery/contact-sheets/socket-attach-points-contact-sheet.webp vs armature-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 ✅, saturation ⚠️ least-saturated of the four but the orange pads carry the accent, thumbnail legibility ✅. Verdict: holds the lineup.
  • Asset sheet: docs/gallery/asset-sheets/socket-attach-points.webp vs collision-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.
  • Generated HTML reviewed (the teaches.split(".") precedent): card alt and 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.
  • Playwright in situ (desktop 1440×900, mobile 390×844): 43 cards, 0 broken images, card image loads at native 1280×720, counter reads "43 EXAMPLES", no horizontal overflow on mobile, thumbnail legible at 333 px. The new card sits directly under modular-kit-snap and lightmap-uv-channel and 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.py raises ValueError: Vector.angle(other): zero length vectors have no valid angle on a mesh with a zero-area face rather than reporting it. It found my degenerate bevels, but via a crash.
  • The docs/VISUAL-STYLE.md WebP 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 against bmesh-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.
  • Chromium fullPage capture 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-symmetry 1 … modular-kit-snap 8) 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.

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>
@github-actions github-actions Bot added documentation Improvements or additions to documentation ci labels Jul 24, 2026
@TMHSDigital
TMHSDigital merged commit 4bfe83b into main Jul 24, 2026
9 checks passed
@TMHSDigital
TMHSDigital deleted the feat/socket-attach-points branch July 24, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant