Skip to content

Give the three Three.js audit gates one owner for their XYZ transform math - #5826

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-5681
Sep 2, 2026
Merged

Give the three Three.js audit gates one owner for their XYZ transform math#5826
atomantic merged 2 commits into
mainfrom
claim/issue-5681

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

  • Extracts the row-major 3×3 THREE.Euler order XYZ transform math that server/lib/threejsModel.js, server/lib/threejsModelPenetration.js and server/lib/threejsModelPhysicalAudit.js each carried a private copy of into one owner, server/lib/threejsTransform.js. The three copies were algebraically identical but textually different, with nothing pinning them together — so the three gates could silently start measuring three different scenes from the one the preview canvas and the exported factory render.
  • Fixes a silently-passing audit. The three copies had already drifted on input hardening: threejsModel.js coerced a non-finite rotation component to 0 degrees, while both audit modules fed the raw value into Math.cos. A stored spec with a NaN/undefined rotation gave those two gates NaN world bounds, and every touch and overlap test against NaN is false — a block sitting flat on the ground came back as floating-part, and a fully buried part as no overlap at all. The shared module takes the coercing behaviour as canonical (non-finite rotation component → 0 degrees, non-finite scale component → 1), which is what the renderer already does with a malformed spec.
  • composeTransform now takes the local TRS object-shaped ({ position, rotationDegrees, scale }) — the shape stored specs already carry, so a whole part passes straight through and the penetration gate's call site is unchanged. The physical audit's two positional call sites were updated.
  • Registers the new module in the server/lib/index.js barrel and server/lib/README.md per the Module Organization rule. invertTransform stays in the penetration gate (one consumer) and partLinear stays in threejsModel.js (a two-line composition specific to that file).
  • IDENTITY_LINEAR / IDENTITY_TRANSFORM are frozen: they were module-private literals before, and exporting them makes one object the frame every walk in the process starts from.

Closes #5681

Test plan

  • New server/lib/threejsTransform.test.js pins the convention against hand-computed values, not a re-implementation: the three single-axis XYZ matrices, right-handed basis-vector rotation on each axis, rotationMatrix([30,40,50]) equalling Rx·Ry·Rz composed separately (catches an order swap), and a rotated+scaled child nested under a rotated parent applied to a point ([3, 2, 0], hand-computed).
  • Each audit suite gains one boundary case proving the hardening actually reached it: threejsModelPenetration.test.js mutates a schema-parsed spec to a non-finite rotation/scale and asserts a finite buried-part fraction still comes back; threejsModelPhysicalAudit.test.js asserts a malformed-transform block resting on the ground audits identically to the well-formed one, with no spurious floating-part.
  • Verified non-vacuous: with the coercion stripped out of threejsTransform.js, all five hardening assertions go red (2 in the audit suites, 3 in the new unit suite), and the physical-audit case specifically flips to the false floating-part report.
  • cd server && npm test — 1833 files / 37345 tests pass. Six unrelated suites (routes/imageGen.*, routes/settings.secretsStrip, services/imageTo3d/trellis2NormalBake, services/sprites/atlas, services/voice/fineTuning) flaked under full-run load; all six pass in isolation both on the unmodified baseline and with this branch applied, so they are pre-existing load flakes, not this change.

… transform math (#5681)

`threejsModel.js`, `threejsModelPenetration.js` and `threejsModelPhysicalAudit.js`
each carried their own row-major 3x3 copy of the same rotation/compose/apply
math, all three claiming to reproduce `THREE.Euler` order 'XYZ' — the composition
the preview canvas and the exported factory actually render with. They were
algebraically identical but textually different with nothing pinning them
together, so the three gates could silently start measuring three different
scenes.

They had already drifted on input hardening: `threejsModel.js` coerced a
non-finite rotation component to 0 degrees, while both audit modules fed the raw
value into `Math.cos`. A stored spec with a `NaN`/`undefined` rotation therefore
gave those two gates `NaN` world bounds, and every touch and overlap test against
`NaN` is false — a block sitting flat on the ground was reported as
`floating-part`, and a fully buried part as no overlap at all.

The new `server/lib/threejsTransform.js` owns the primitives and takes the
coercing behaviour as canonical (a non-finite rotation component reads as 0
degrees, a non-finite scale component as 1), so the audits measure the same scene
the renderer draws. `composeTransform` takes the local TRS object-shaped, which
is what stored specs already carry, so a whole part passes straight through.
`IDENTITY_LINEAR` and `IDENTITY_TRANSFORM` used to be module-private literals in
each of the three gates; exporting them makes one object the frame every walk in
the process starts from, so a stray write would move it for every later caller.
Freeze both, and pin it with a test.
@atomantic
atomantic merged commit 70ae3ef into main Sep 2, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5681 branch September 2, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Collapse the three copies of the THREE.Euler XYZ transform math in server/lib/threejsModel*.js

1 participant