Skip to content

fix(assembly): treat a joint origin as a pivot, not a translation - #686

Closed
w1ne wants to merge 1 commit into
developfrom
fix/joint-pivot-rotation-center
Closed

fix(assembly): treat a joint origin as a pivot, not a translation#686
w1ne wants to merge 1 commit into
developfrom
fix/joint-pivot-rotation-center

Conversation

@w1ne

@w1ne w1ne commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The defect

forwardKinematics composed T(o) . M with no T(-o), so a joint's origin leaked in as a bare translation instead of acting as a pivot. Every joint with a non-origin pivot displaced its child by the pivot vector — visible at pose 0, where the child must sit exactly where it was modeled.

Measured against production (POST api.kernelcad.com/__kernelcad/export?format=stl, binary STL parsed locally). Hinge with pivot [5,20,16], posed at elbow: 0:

Model     arm  [ 5, 15, 12] .. [55, 25, 20]
Exported  arm  [10, 35, 28] .. [60, 45, 36]
Delta          [+5, +20, +16]  ==  the pivot, exactly

The Y figure alone is decisive: the joint spins about Y, and a rotation about Y cannot change a Y coordinate — yet y moved 15..25 → 35..45. Controls confirm it: pivot [5,0,16] makes Y correct and leaves X/Z wrong by 5 and 16; pivot [0,0,0] makes all three correct.

Knock-on effects: the interference validator and swept-collision score displaced solids, so a hinge that collides at 30° swept clean across 0–90°.

The fix

Conjugate the motion by the origin — T(o) . M . T(-o) — for all four joint kinds.

This is the convention the mate solver already uses: see composeChildTransform() in src/modeling/mates/solver.ts, which conjugates the joint frame by the parent and child connector origins the same way. Transform.rotationAroundPivot() in se3.ts was already the correct primitive; FK just never called it. A mate-built hinge at elbow: 0 returns the modeled position today — only the joint-primitive path was wrong.

Why it shipped

Every pre-existing .revolute(...) test used origin: [0, 0, 0], where T(o) is identity and the defect is invisible. The one FK test asserted expect(solved.transform('link')).toBeDefined() — it never checked a position.

Verification

  • src/modeling/capture/forwardKinematics.test.ts — 7 new tests, all with a non-zero pivot, asserting actual transformed points. All 7 fail without this change, with exactly the displacements measured above (expected 35 to be close to 15, expected 10 to be close to 5).
  • src/modeling/capture, src/modeling/mates, src/kinematic — 43 files / 428 tests pass, no regressions.

Note for release

The export API and every MCP tool run the engine vendored into kernelCAD-server at 0.13.0 (web is 0.15.0). This fix does not reach customers until that vendored dist is rebuilt and bumped.

Parts are modeled in assembly coordinates, so a joint's `origin` is a PIVOT
POINT. forwardKinematics composed T(o) . M with no T(-o), so the origin
leaked in as a bare translation and every joint with a non-origin pivot
displaced its child by the pivot vector — visible at pose 0, where the child
must sit exactly where it was modeled.

A hinge with pivot [5,20,16] posed at elbow=0 exported its arm at
[10,35,28]..[60,45,36] instead of the modeled [5,15,12]..[55,25,20]. The Y
figure alone is decisive: the joint spins about Y, and a Y rotation cannot
change a Y coordinate, yet y moved 15..25 -> 35..45.

Conjugate the motion by the origin — T(o) . M . T(-o) — for all four joint
kinds. This is the convention the mate solver already uses; see
composeChildTransform() in src/modeling/mates/solver.ts, which conjugates the
joint frame by the parent and child connector origins the same way.
Transform.rotationAroundPivot() in se3.ts was already the correct primitive.

Every pre-existing `.revolute(...)` test used `origin: [0, 0, 0]`, where T(o)
is identity and the defect is invisible, and the one FK test asserted only
`toBeDefined()`. The new tests all use a non-zero pivot and assert positions:
all 7 fail without this change.
@w1ne
w1ne enabled auto-merge August 25, 2026 14:33
@w1ne
w1ne disabled auto-merge August 25, 2026 14:46
@w1ne

w1ne commented Aug 25, 2026

Copy link
Copy Markdown
Owner Author

Closing — the premise of this PR is wrong. forwardKinematics was correct as written; I misread which convention it implements.

tests/unit/assemblies/forwardKinematics.test.ts makes it explicit: elbow.point([0,0,0]) must land at world (0,0,10) for a joint with origin: [0,0,10]. That is the URDF convention — a child link's geometry is expressed in its own link frame, and the joint origin is the parent→child frame offset. T(o) . R is exactly right for that, and the whole robotics stack depends on it: this change also broke checkReachableMultiDof ("does not regress the rest pose to unreachable"), because numeric IK walks those link frames.

Both tests pass on develop and fail with this change. Verified locally after backing it out.

What I actually hit is a convention collision, not a math error:

  • .revolute() / joint primitives → URDF link frames; child modeled about its own origin.
  • .mate() + connectors → in-place assembly; composeChildTransform() conjugates by the connector origins so pose 0 preserves the modeled position.

I modeled parts in place (box(...).translate(5, 15, 12)) and then drove them with .revolute(). The engine applied URDF semantics to in-place geometry and produced silently displaced parts — arm at [10,35,28] instead of [5,15,12] at pose 0, with the interference validator and swept-collision then scoring the displaced solids.

So the real defect is that mixing the two conventions is undetectable and unhinted. Nothing in lookup_api, the authoring skill, or the joint docstrings says .revolute() expects link-local geometry, and no diagnostic fires when a part with a non-identity modeled transform becomes the child of a joint primitive. That is worth fixing — as a diagnostic, not by changing the math. Filing separately.

Apologies for the noise on CI.

@w1ne w1ne closed this Aug 25, 2026
@w1ne
w1ne deleted the fix/joint-pivot-rotation-center branch August 25, 2026 14:50
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.

1 participant