Modular Kit Snap
A tiling corridor kit whose open-end boundary verts snap to the tile grid, so instances at 4 m multiples join with zero gap or overlap.
examples/socket-attach-points/
A survey drone whose named SKT_ empties are the spawn contract: modules parented with an identity local transform seat exactly on their mount pads.
Rendered headless by the example itself. Select it to enlarge.
tags mesh transforms game-pipeline instancing
runs on 4.5 LTS · 5.1 · 5.2 LTS
blender --background --python examples/socket-attach-points/socket_attach_points.py --
falsify it Breaks the contract on purpose and must exit 3
blender --background --python examples/socket-attach-points/socket_attach_points.py -- --skip-mpi
A runnable example building the thing a spawn system actually leans on: named socket empties parented into an asset, so a module dropped onto a socket with an identity local transform lands exactly where the artist put it, oriented the way the naming convention promises. Every clause in that sentence is a matrix identity, and every one of them drifts silently — 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.
The asset, for reuse: Drone.Survey, an 0.86 m span survey quadcopter — a lofted fuselage with a vented deck and canopy face, four canted booms with finned motor cans and nav lights, and a two-rail landing gear. Seven mount pads carry seven sockets, and four interchangeable modules seat on them:
| Socket | Mount normal | Module |
|---|---|---|
SKT_Rotor.FL/FR/RL/RR | 8° canted outward | Mod.Rotor (twisted, tapered blades) |
SKT_Camera | belly, −Z | Mod.CamPod (gimbal ball + lens) |
SKT_Mast | dorsal, +Z | Mod.Mast (antenna, dish, beacon) |
SKT_Battery | rear, tilted down | Mod.Battery (cell block + gauge) |
Origin is the skid contact plane centre (z == 0 is where it rests, +X is forward), transforms are identity by construction, datablocks live under Drone.Survey.* and SKT_*, and accents are per-face material slots rather than extra objects. To use it: instantiate a module, parent it to the socket, zero the local transform. That is the whole API — and it only works because the socket matrices hold.
Socket convention (encoded in the SKT_ prefix):
Y × Z, so the basis is right-handed (det == +1).Pipeline arc neighbours: pivot and origin discipline in prop-origin-transform, parent-inverse under animation in parent-inverse-orrery, tile-grid boundary contracts in modular-kit-snap, collision packaging in collision-hull-proxy.
What it witnesses (all closed form or independently re-derived):
matrix_world values equal root.matrix_world @ authored_local within 1e-6 (measured 1.788e-07); every basis is orthonormal (Gram error 3.576e-07) and right-handed (det − 1 = 2.980e-07).dot − 1 = 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place.root.matrix_world @ authored_local (2.384e-07) and every module stays on its socket (0.0).Drone.Survey.* / SKT_* names, no default datablocks, min z == 0.00e+00.transform_apply on the root resets 7/7 children's matrix_parent_inverse to identity and hands each child the root's scale locally (measured 0.35 for an applied 1.35). World matrices survive to float32; local transforms do not. A spawn system that reads matrix_local, or an exporter that trusts "transforms are applied", reads different numbers after an artist freezes the rig. Both halves are asserted, so the behaviour cannot change under us silently.bm.normal_update() does not fix winding. It recomputes normals from the existing winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel rather than an obvious hole; bmesh.ops.recalc_face_normals is what fixes it, and Part.finish now always calls it.degenerate-bevel-weld witnesses. 2.2 mm is clean. Note that examples/gallery_asset_quality.py raises ValueError: zero length vectors have no valid angle on such a mesh rather than reporting it.What each check catches on failure (every one probed, with the measured error): sockets parented without the parent-inverse — worst jump 0.690128 m (exit 3); a left-handed basis from z.cross(y) — det == −1.000000 (exit 3); a rotor pad built flat instead of canted — 1.395e-01 off the Newell normal (exit 4); an 11° roll on the basis — 1.917e-01 off the up rule (exit 4); a module nudged 4 mm off its socket (exit 5); a default Cube datablock name (exit 6); a socket left unparented so it ignores the re-pose — 1.417e+00 (exit 7); a child selected during the apply — 1.252e+00 to 2.244e+00 of socket drift (exit 8).
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same measured deviations to every printed digit, and the same framing numbers on the render path.
Render as proof: the drone hovering over the dark stage with every module seated, the orange mount pads reading as the socket signature. The livery is two-tone — a gloss cobalt fuselage shell (the pads' complement, so the sockets pop), off-white arm fairings and spinners, graphite deck and trim — and the booms carry a procedural twill carbon weave under a clear coat. The camera looks down at the airframe, so the floor is the backdrop: a small close key keeps its falloff on the drone and the warm wedge pools on the floor behind it. The falsification variant (--falsify) parents the sockets without matrix_parent_inverse: the pads are left bare, the rotors and mast vanish from the airframe, and the camera pod and battery hang in space near the top of frame. A broken matrix is not a subtle pixel difference here — the asset comes apart.
blender --background --python socket_attach_points.py --
blender --background --python socket_attach_points.py -- --skip-mpi # falsifier: sockets parented without matrix_parent_inverse; exits 3
blender --background --python socket_attach_points.py -- --output drone.png
blender --background --python socket_attach_points.py -- --falsify adrift.png
blender --background --python socket_attach_points.py -- --probe
The --output render path additionally gates framing via examples/gallery_framing.py (fill 0.741x, margins 0.131/0.128/0.172/0.156, no edge touched — pulled back from 0.881x so the rotor tips clear gallery card crops) and the asset floors via examples/gallery_asset_quality.py (32 materials, edge90 0.027, no default names). --falsify is a diagnostic, not a gallery hero: it measures and prints framing but does not enforce the Layer 1 band — modules leave the frame on purpose. --falsify and --probe still exit 0.
Per-script sequential checks. 9 is a valid check code; there is no rule against it. 10 is the shared framing helper. 11 is the shared asset-quality helper.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Uncaught exception (FATAL wrapper) |
| 2 | argparse / usage |
| 3 | Socket world matrix, orthonormal basis, or determinant (--skip-mpi lands here) |
| 4 | Socket pad normal, origin, or up-axis rule |
| 5 | Module seating / mount axis / leftover local transform |
| 6 | Population, namespace, default datablock name, SKT_ prefix, or skid plane |
| 7 | Re-pose: socket or module lost the root |
| 8 | Parent-inverse clear, child scale pushdown, or post-apply module drift |
| 9 | --output produced no file |
| 10 | Gallery framing violation |
| 11 | Gallery asset-quality violation |
The blender-smoke workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch). Smoke does not pass --output, --falsify, or --probe. Its catalog falsifier is --skip-mpi (expect exit 3): the sockets are parented without matrix_parent_inverse, the defect --falsify renders.