Skip to content

feat(sdk): object.attach(child) keeps the child where it stands; the editor uses it (#758) - #784

Merged
pasquelin merged 11 commits into
developfrom
758-node-attach
Sep 26, 2026
Merged

pasquelin merged 11 commits into
developfrom
758-node-attach

Conversation

@pasquelin

@pasquelin pasquelin commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #758

What changed

  • SceneNode.attach(child) (packages/sdk-core/src/scene/core/node.ts, logic in nodeAttach.ts as nodeCopy.ts does for copy): re-parents a node while keeping its world matrix. Both world matrices are resolved through the node's own updateWorldMatrix; the new local matrix is, in the reference's order, the inverse of the parent's world matrix times the former parent's world matrix times the child's local matrix (invertMatrix4, multiplyMatrix4), split into position, rotation and scale by the existing decomposeMatrix4, then add. No second inverse or decompose. When a subclass's add declines the child, its pose is left as it was; Object3D.attach declines itself before any work.
  • Object3D.attach reads the new pose from the tree back into position, quaternion and scale without firing their listeners (angles follow the quaternion on the next read), then tells the world once; lookAt reads its pose back through the same readPose. The field-to-tree binding moved into objectPose.ts (bindPose), next to readPose.
  • nodeError.ts refuseSceneRoot replaces three copies of the scene-root check in node.ts.
  • The editor's reparentCommand (site/app/editor/commands.ts) calls parent.attach(object). Its own inverse and decompose are gone; undo still puts back the saved local pose. attachCommand (a plain add) is renamed addCommand so it does not read as the world-preserving attach.
  • docs/SDK.md documents attach, and every api.<language>.json translates it for SceneNode and Object3D.
  • Bundle size: the maths-only bundle stays at 5_082 bytes (tests/integration/sdk-facade.test.ts unchanged). The attach scratch arrays are marked /* @__PURE__ */ so a bundle that never attaches drops them.
  • Declared limit: a sheared result loses its shear in the split into position, rotation and scale, and a parent scaled to zero inverts to the zero matrix, both as the reference does (said in docs/SDK.md and nodeAttach.ts).

Proof

  • node.test.ts: a child under a parent that is moved, rotated and scaled non-uniformly, attached to another such parent, keeps its world matrix to within 1e-12 and still recomposes it after an update. Attached back, its local matrix returns. A cycle is refused.
  • object3d.test.ts: same check through Object3D. position, quaternion and scale recompose the kept world matrix, the angles follow, and attaching a node to itself is declined with nothing changed.
  • object3d.test.ts: 16 attaches of unrounded poses, under automatic and manual update, give to the bit the position, quaternion, scale (and, under manual update, the matrix) of the reference's order, inverse(new parent world) x old parent world x local. The test fails on cd05303 (inverse(new parent world) x child world: last-bit differences) and passes after 9bcf2b7.
  • scripts/docs-scene-editor.test.ts: the editor's reparent keeps the world position, and undo puts back the old parent and the exact saved pose. The existing editor tests still pass.
  • After git submodule update --init, pnpm run build:native, pnpm run compile:caches and pnpm run build: pnpm run check:changed and pnpm run test:changed both pass with no failures. pnpm run generate:api and pnpm run check:i18n pass. tsc on the core, site, tools and root configs passes, and eslint on the changed files is clean. node scripts/check-pr-size.ts: 239 hand-written lines.
  • After merging origin/develop (with Streaming without holes: texture levels survive a device loss in the world cache and yield to the pages (#745) #777): pnpm run validate --group quick passes.

Local review before push

  • Simplification pass: the real simplify skill, run twice (coder, then reviewer; 4 agents each: reuse, simplification, efficiency, altitude). The coder's run fixed five things: Object3D.attach no longer decomposes a second time (it reads the tree's slots quietly, with one world notice); a declined attach is guarded; the attach scratch is marked @__PURE__, so the maths-only bundle is back at 5_082; the reformatting used to fit 200 lines is reverted, and the room comes from nodeAttach.ts, objectPose.ts and one root check; one shared closeness helper. The reviewer's run fixed three: lookAt reads back through readPose (quiet, one notice) instead of a listener round trip; Object3D.attach declines only itself (child === this) before any work; assertClose checks lengths with a plain loop. Kept on purpose: the inverse computed before a declined add, the copy of the local matrix (16 numbers, the pose under manual update), the immediate subtree update.
  • Correctness review: the real code-review --fix skill, run twice. The coder's run renamed attachCommand to addCommand and named the refused root action. The reviewer's run found 7 findings and fixed the main one: attach built the local matrix as inverse(new parent world) x child world, not in the reference's order (inverse(new parent world) x old parent world x child local). A throwaway comparison with the witness found 200 of 200 random attaches off in the last bits before the fix and 0 after; a bit-exact test in object3d.test.ts now pins the order. Left out: the adopted-storage matrix of TransformNode (outside this issue); an editor undo restoring only position, quaternion and scale for a manual-update object (editor objects auto-update); a pose notice after a subclass's add declines a child (values unchanged); translations of SceneNode.attach adding "its local pose rewritten" (true, wider than the English); the local matrix copy under auto update (negligible). Not covered by tests: the manual-update path and a subclass's add declining at the SceneNode level. CI fix (native job, engine-structure.test.ts): nodeAttach.fixture.ts imported node:assert/strict, which sdk-core's non-test files may not. As in the other sdk-core fixtures, it now imports nothing and returns the first mismatch (mismatch), and the tests assert on it; the structure test is unchanged.
  • Auditor list: every To do and Proof item of object.attach re-parents a node and keeps where it is in the world #758 is delivered and the body says Closes #758; the diff follows the lead's design note (one decompose, existing matrix helpers); the three tests fail on develop (no attach); labels sdk, 🟡 normal, in review; docs/SDK.md, the API reference and 14 translations follow; no image, format, streaming or example change; maths-only bundle 5_082.

Not proven / left out

  • attach on a node whose matrix.elements point at the caller's own storage (the adopted-matrix path of TransformNode) is not handled: the storage is neither read before nor written after.
  • No browser proof: attach is a CPU scene-graph call; WebGPU and WebGL2 see an ordinary pose change.

Lead verification

  • object.attach(child) re-parents a node while keeping its world matrix: delivered in packages/sdk-core/src/scene/core/node.ts:105 (attach, in the file's one-line doc style) and packages/sdk-core/src/scene/core/nodeAttach.ts:20 (attachSceneNode: the new parent's inverse × the old parent's world × the child's local, one decompose, a declined attach leaving the pose untouched), with Object3D.attach reading the pose back quietly (world/object/objectPose.ts, lookAt sharing that readback); proved by attach moves a child under another parent where it stands in the world, attach keeps the world matrix, and position, rotation and scale hold the new pose and attach gives the reference's pose to the bit: new parent's inverse × old parent × local (16 attaches, automatic and manual update, fails on the first order).
  • Documented in docs/SDK.md, the API reference and every translation: delivered in docs/SDK.md (attach beside add and reparent, the shear limit stated) and the 14 site/content/reference/api.<lang>.json; proved by check:i18n and the reference tests.
  • The editor's reparentCommand uses it: delivered in site/app/editor/commands.ts:62-73 (redo is parent.attach(object), its own invert, multiply and decompose removed; undo keeps its exact saved pose); proved by the editor's reparent, undo and redo test in scripts/docs-scene-editor.test.ts.
  • rounds: 2 (coder ↔ reviewer: the reviewer's simplify and code-review fixes, then the pinning test).
  • Whole promise: the one To-do and its Proof delivered.
  • Tests that bite: the attach tests fail on develop (no attach) and the bit-exact one on the earlier order.
  • No image loss: no drawn path touched; a node moved by attach is an ordinary pose change for both backends.
  • Reuse: invertMatrix4, multiplyMatrix4, decomposeMatrix4 (once) and the quiet Quaternion.set reused; refuseSceneRoot gathers three copies of the root check; searched by the /simplify reuse agents, no twin.
  • Docs: docs/SDK.md, the API reference and its 14 translations.
  • Path: in review set; to measure after the merge (a public engine member). The maths-only bundle keeps its 5_082 bytes (@__PURE__ scratch). Streaming without holes: rules and objectives for geometry, memory and shadows #483 checklist and CONTRIBUTING.md §Streaming, memory and shadows: a call, no per-frame work.

@pasquelin
pasquelin merged commit e264571 into develop Sep 26, 2026
8 checks passed
@pasquelin
pasquelin deleted the 758-node-attach branch September 26, 2026 07:29
@pasquelin pasquelin added the audited Image proved and promise kept (recette) label Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audited Image proved and promise kept (recette)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant