fix: stop shrinking CAD models rotated off-axis - #961
Merged
Conversation
getObjectBoundsRelativeToParent transformed each mesh's bounding box into world space and then back into the parent's frame using two separate Box3.applyMatrix4 calls. Box3.applyMatrix4 re-derives an axis-aligned box around the transformed corners, so it is not invertible: for any rotation that is not a multiple of 90 degrees the round trip widened the box twice rather than cancelling. A 9.5 x 6.05 model on a component rotated 30 degrees measured 14.74 x 14.28, and getCadModelFitScale then shrank the model to 0.42 to fit that phantom size into its declared `size`. Worst case was 45 degrees at 0.39. Right angles were unaffected, which is why this survived: an AABB is rotation-invariant there, and the headless circuit-json-to-gltf pipeline applies rotation after fitting, so no snapshot could catch it. Compose the two matrices and apply them once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The same switch at 0, 30, 45 and 90 degrees. Its declared size matches its mesh, so every instance must fill its silkscreen outline and meet its pads at every angle; before the fit fix the 30 and 45 degree parts rendered at 0.42 and 0.39. Each instance declares `size` deliberately -- both renderers skip the fit when it is absent, which is why core's own fixture for this part never showed the defect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
seveibar
approved these changes
Aug 5, 2026
|
Thank you for your contribution! 🎉 PR Rating: ⭐⭐ Track your contributions and see the leaderboard at: tscircuit Contribution Tracker |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A CAD model that declares a
sizematching its own mesh is shrunk whenever its component sits at a rotation that is not a multiple of 90°.This 6×6mm tactile switch (9.5mm lead span) is the same part four times, at 0°, 30°, 45° and 90°. Only the right-angle ones meet their pads:
Cause
getObjectBoundsRelativeToParentmeasured each mesh by transforming its bounding box into world space and then back into the parent's frame, as two separate calls:Box3.applyMatrix4re-derives an axis-aligned box around the eight transformed corners, so it is not invertible. Applying a rotation and then its inverse doesn't cancel — each call grows the box:getCadModelFitScalethen divides the declaredsizeby that inflated measurement and shrinks the model to 0.42 to make it "fit".Right angles are unaffected because an axis-aligned box is rotation-invariant there.
Fix
Compose the two matrices and apply them once, so the ancestor transform genuinely cancels:
Rotations at or below the measured object are still included — the existing
fit includes loader transform group rotation in measured boundstest covers that and still passes.Why this went unnoticed
Two things hid it:
sizeis optional, and the fit is skipped without it (if (!targetSize) return [1,1,1]). Parts that don't declare one were never affected.circuit-json-to-gltfapplies component rotation as a scene-graph property afterfitMeshToCadBounds, so its fit always measures an unrotated mesh. I confirmed it returns9.500 × 15.000 × 6.050at 0°, 30° and 45°. Snapshot tests,tsci buildand poppygl renders are all correct — so no existing test could have caught this. It is visible only in the browser viewer.Tests
tests/cad-model-fit.test.ts— asserts scale 1.0 at 0/15/30/45/90/180°. Verified it fails without the fix.stories/Bugs/RotatedModelFitScale.stories.tsx— visual regression. Pads userotated_rectwithccw_rotationand the silkscreen outline is rotated point-by-point, so both turn exactly with the part and a shrunken model visibly pulls away from them. Each instance declaressizedeliberately, since the fit is skipped without it.Full suite matches
mainexactly: the 4 pre-existing failures (faux-board z defaults, Atari SVG outline) are unchanged, and the fit tests go 4 → 5 passing.No new Circuit JSON properties are used; everything is in the pinned
circuit-json@0.0.446.