Repository navigation
fix: use finite bend regions for outlined flex boards - #4288
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
8febef1 to
c8deb84
Compare
| test("finite bend folds only its tail before CAD insertion, including a translated board", async () => { | ||
| for (const offset of [ | ||
| { x: 0, y: 0 }, | ||
| { x: 100, y: -70 }, | ||
| ]) { | ||
| const render = async (bent: boolean) => { | ||
| const { circuit } = getTestFixture() | ||
| circuit.add( | ||
| <board | ||
| material="flex" | ||
| layers={2} | ||
| thickness={0.12} | ||
| solderMaskColor="#cc9b32" | ||
| routingDisabled | ||
| schematicDisabled | ||
| pcbX={offset.x} | ||
| pcbY={offset.y} | ||
| outline={outline.map((point) => ({ | ||
| x: point.x + offset.x, | ||
| y: point.y + offset.y, | ||
| }))} | ||
| > | ||
| {bent && ( | ||
| <pcbbend | ||
| x1={-20} | ||
| y1={20} | ||
| x2={-12} | ||
| y2={20} | ||
| bendAngle={90} | ||
| bendRadius={1} | ||
| bendSide="left" | ||
| /> | ||
| )} | ||
| <resistor | ||
| name="RL" | ||
| resistance="1k" | ||
| footprint="0402" | ||
| pcbX={-16} | ||
| pcbY={25} | ||
| /> | ||
| <resistor | ||
| name="RR" | ||
| resistance="1k" | ||
| footprint="0402" | ||
| pcbX={16} | ||
| pcbY={25} | ||
| /> | ||
| <silkscreentext | ||
| text="RL" | ||
| pcbX={-16} | ||
| pcbY={27.5} | ||
| fontSize={1.6} | ||
| anchorAlignment="center" | ||
| /> | ||
| <silkscreentext | ||
| text="RR" | ||
| pcbX={16} | ||
| pcbY={27.5} | ||
| fontSize={1.6} | ||
| anchorAlignment="center" | ||
| /> | ||
| </board>, | ||
| ) | ||
| await circuit.renderUntilSettled() | ||
| return circuit | ||
| } | ||
| const flat = await render(false) | ||
| const folded = await render(true) | ||
| expect(folded.db.pcb_component.list()).toEqual(flat.db.pcb_component.list()) | ||
| const [left, right] = folded.db.cad_component.list() | ||
| const [flatLeft, flatRight] = flat.db.cad_component.list() | ||
| expect(left!.position.z).toBeCloseTo(6 - Math.PI / 4, 7) | ||
| expect(left!.rotation!.x).toBeCloseTo(90, 7) | ||
| expect(left!.position.x).toBeCloseTo(flatLeft!.position.x, 7) | ||
| expect(right!.position).toEqual(flatRight!.position) | ||
| for (const coordinate of ["x", "y", "z"] as const) | ||
| expect(right!.rotation![coordinate]).toBeCloseTo( | ||
| flatRight!.rotation![coordinate], | ||
| 7, | ||
| ) | ||
| const restored = transformCircuitJsonCadComponents( | ||
| folded.getCircuitJson(), | ||
| { foldPcbs: false }, | ||
| ).filter((element) => element.type === "cad_component") | ||
| for (let i = 0; i < restored.length; i++) | ||
| for (const coordinate of ["x", "y", "z"] as const) | ||
| expect(restored[i]!.position[coordinate]).toBeCloseTo( | ||
| flat.db.cad_component.list()[i]!.position[coordinate], | ||
| 7, | ||
| ) | ||
|
|
||
| for (const circuit of [flat, folded]) { | ||
| await expect(circuit).toMatch3dSnapshot(import.meta.path, { | ||
| // The exporter selects the pose from core's emitted CAD fold flags. | ||
| gltf: { boardTextureResolution: 1024 }, | ||
| snapshotSuffix: circuit === flat ? "flat" : "folded", | ||
| diffTolerance: 0.001, | ||
| poppygl: { | ||
| width: 1000, | ||
| height: 760, | ||
| // Camera points are right-handed glTF (+Y up, mm), following | ||
| // getBestCameraPosition's Circuit JSON -> glTF mapping (-X, Z, Y). | ||
| camPos: [55 - offset.x, 60, -65 + offset.y], | ||
| lookAt: [-offset.x, 2, 9 + offset.y], | ||
| up: "y+", | ||
| fov: 35, | ||
| backgroundColor: "#f2f3f5", | ||
| ambient: 0.45, | ||
| grid: undefined, | ||
| }, | ||
| }) | ||
| } | ||
| } | ||
| }) |
There was a problem hiding this comment.
This test file contains a single test(...) block that internally loops over multiple offsets (origin and translated board), effectively running two distinct test scenarios inside one test. The rule states that a *.test.ts file may have AT MOST one test(...), and after that the user should split into multiple numbered files (e.g. finite-segment-cad-placement1.test.tsx, finite-segment-cad-placement2.test.tsx). While there is technically only one test() call, the for loop over [{ x: 0, y: 0 }, { x: 100, y: -70 }] bundles two independent scenarios into one test, which defeats the purpose of the rule. Each offset scenario should be extracted into its own numbered test file.
| test("finite bend folds only its tail before CAD insertion, including a translated board", async () => { | |
| for (const offset of [ | |
| { x: 0, y: 0 }, | |
| { x: 100, y: -70 }, | |
| ]) { | |
| const render = async (bent: boolean) => { | |
| const { circuit } = getTestFixture() | |
| circuit.add( | |
| <board | |
| material="flex" | |
| layers={2} | |
| thickness={0.12} | |
| solderMaskColor="#cc9b32" | |
| routingDisabled | |
| schematicDisabled | |
| pcbX={offset.x} | |
| pcbY={offset.y} | |
| outline={outline.map((point) => ({ | |
| x: point.x + offset.x, | |
| y: point.y + offset.y, | |
| }))} | |
| > | |
| {bent && ( | |
| <pcbbend | |
| x1={-20} | |
| y1={20} | |
| x2={-12} | |
| y2={20} | |
| bendAngle={90} | |
| bendRadius={1} | |
| bendSide="left" | |
| /> | |
| )} | |
| <resistor | |
| name="RL" | |
| resistance="1k" | |
| footprint="0402" | |
| pcbX={-16} | |
| pcbY={25} | |
| /> | |
| <resistor | |
| name="RR" | |
| resistance="1k" | |
| footprint="0402" | |
| pcbX={16} | |
| pcbY={25} | |
| /> | |
| <silkscreentext | |
| text="RL" | |
| pcbX={-16} | |
| pcbY={27.5} | |
| fontSize={1.6} | |
| anchorAlignment="center" | |
| /> | |
| <silkscreentext | |
| text="RR" | |
| pcbX={16} | |
| pcbY={27.5} | |
| fontSize={1.6} | |
| anchorAlignment="center" | |
| /> | |
| </board>, | |
| ) | |
| await circuit.renderUntilSettled() | |
| return circuit | |
| } | |
| const flat = await render(false) | |
| const folded = await render(true) | |
| expect(folded.db.pcb_component.list()).toEqual(flat.db.pcb_component.list()) | |
| const [left, right] = folded.db.cad_component.list() | |
| const [flatLeft, flatRight] = flat.db.cad_component.list() | |
| expect(left!.position.z).toBeCloseTo(6 - Math.PI / 4, 7) | |
| expect(left!.rotation!.x).toBeCloseTo(90, 7) | |
| expect(left!.position.x).toBeCloseTo(flatLeft!.position.x, 7) | |
| expect(right!.position).toEqual(flatRight!.position) | |
| for (const coordinate of ["x", "y", "z"] as const) | |
| expect(right!.rotation![coordinate]).toBeCloseTo( | |
| flatRight!.rotation![coordinate], | |
| 7, | |
| ) | |
| const restored = transformCircuitJsonCadComponents( | |
| folded.getCircuitJson(), | |
| { foldPcbs: false }, | |
| ).filter((element) => element.type === "cad_component") | |
| for (let i = 0; i < restored.length; i++) | |
| for (const coordinate of ["x", "y", "z"] as const) | |
| expect(restored[i]!.position[coordinate]).toBeCloseTo( | |
| flat.db.cad_component.list()[i]!.position[coordinate], | |
| 7, | |
| ) | |
| for (const circuit of [flat, folded]) { | |
| await expect(circuit).toMatch3dSnapshot(import.meta.path, { | |
| // The exporter selects the pose from core's emitted CAD fold flags. | |
| gltf: { boardTextureResolution: 1024 }, | |
| snapshotSuffix: circuit === flat ? "flat" : "folded", | |
| diffTolerance: 0.001, | |
| poppygl: { | |
| width: 1000, | |
| height: 760, | |
| // Camera points are right-handed glTF (+Y up, mm), following | |
| // getBestCameraPosition's Circuit JSON -> glTF mapping (-X, Z, Y). | |
| camPos: [55 - offset.x, 60, -65 + offset.y], | |
| lookAt: [-offset.x, 2, 9 + offset.y], | |
| up: "y+", | |
| fov: 35, | |
| backgroundColor: "#f2f3f5", | |
| ambient: 0.45, | |
| grid: undefined, | |
| }, | |
| }) | |
| } | |
| } | |
| }) | |
| test("finite bend folds only its tail before CAD insertion", async () => { | |
| const offset = { x: 0, y: 0 } | |
| const render = async (bent: boolean) => { | |
| const { circuit } = getTestFixture() | |
| circuit.add( | |
| <board | |
| material="flex" | |
| layers={2} | |
| thickness={0.12} | |
| solderMaskColor="#cc9b32" | |
| routingDisabled | |
| schematicDisabled | |
| pcbX={offset.x} | |
| pcbY={offset.y} | |
| outline={outline.map((point) => ({ | |
| x: point.x + offset.x, | |
| y: point.y + offset.y, | |
| }))} | |
| > | |
| {bent && ( | |
| <pcbbend | |
| x1={-20} | |
| y1={20} | |
| x2={-12} | |
| y2={20} | |
| bendAngle={90} | |
| bendRadius={1} | |
| bendSide="left" | |
| /> | |
| )} | |
| <resistor | |
| name="RL" | |
| resistance="1k" | |
| footprint="0402" | |
| pcbX={-16} | |
| pcbY={25} | |
| /> | |
| <resistor | |
| name="RR" | |
| resistance="1k" | |
| footprint="0402" | |
| pcbX={16} | |
| pcbY={25} | |
| /> | |
| <silkscreentext | |
| text="RL" | |
| pcbX={-16} | |
| pcbY={27.5} | |
| fontSize={1.6} | |
| anchorAlignment="center" | |
| /> | |
| <silkscreentext | |
| text="RR" | |
| pcbX={16} | |
| pcbY={27.5} | |
| fontSize={1.6} | |
| anchorAlignment="center" | |
| /> | |
| </board>, | |
| ) | |
| await circuit.renderUntilSettled() | |
| return circuit | |
| } | |
| const flat = await render(false) | |
| const folded = await render(true) | |
| expect(folded.db.pcb_component.list()).toEqual(flat.db.pcb_component.list()) | |
| const [left, right] = folded.db.cad_component.list() | |
| const [flatLeft, flatRight] = flat.db.cad_component.list() | |
| expect(left!.position.z).toBeCloseTo(6 - Math.PI / 4, 7) | |
| expect(left!.rotation!.x).toBeCloseTo(90, 7) | |
| expect(left!.position.x).toBeCloseTo(flatLeft!.position.x, 7) | |
| expect(right!.position).toEqual(flatRight!.position) | |
| for (const coordinate of ["x", "y", "z"] as const) | |
| expect(right!.rotation![coordinate]).toBeCloseTo( | |
| flatRight!.rotation![coordinate], | |
| 7, | |
| ) | |
| const restored = transformCircuitJsonCadComponents( | |
| folded.getCircuitJson(), | |
| { foldPcbs: false }, | |
| ).filter((element) => element.type === "cad_component") | |
| for (let i = 0; i < restored.length; i++) | |
| for (const coordinate of ["x", "y", "z"] as const) | |
| expect(restored[i]!.position[coordinate]).toBeCloseTo( | |
| flat.db.cad_component.list()[i]!.position[coordinate], | |
| 7, | |
| ) | |
| for (const circuit of [flat, folded]) { | |
| await expect(circuit).toMatch3dSnapshot(import.meta.path, { | |
| // The exporter selects the pose from core's emitted CAD fold flags. | |
| gltf: { boardTextureResolution: 1024 }, | |
| snapshotSuffix: circuit === flat ? "flat" : "folded", | |
| diffTolerance: 0.001, | |
| poppygl: { | |
| width: 1000, | |
| height: 760, | |
| // Camera points are right-handed glTF (+Y up, mm), following | |
| // getBestCameraPosition's Circuit JSON -> glTF mapping (-X, Z, Y). | |
| camPos: [55 - offset.x, 60, -65 + offset.y], | |
| lookAt: [-offset.x, 2, 9 + offset.y], | |
| up: "y+", | |
| fov: 35, | |
| backgroundColor: "#f2f3f5", | |
| ambient: 0.45, | |
| grid: undefined, | |
| }, | |
| }) | |
| } | |
| }) | |
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
|
Thank you for your contribution! 🎉 PR Rating: ⭐⭐ Track your contributions and see the leaderboard at: tscircuit Contribution Tracker |
With the U-shaped flex board in tscircuit/tscircuit#5277, a bend across the left tail also folds the right resistor because core does not provide an outline to flex-utils. Adopt
@tscircuit/flex-utils0.0.6 and pass the emitted board outline after subtracting the final board center. RL folds 90°, while RR's CAD position and rotation stay flat. PCB records remain unchanged.Also adopt released
circuit-json-to-gltf0.0.137, which includes finite board-mesh folding from tscircuit/circuit-json-to-gltf#229. The regression uses core's existingtoMatch3dSnapshotpath: emitted Circuit JSON → GLB export → PoppyGL. The snapshots show actual board textures and resistor models, and the exporter selects the pose from core's emitted CAD fold flags.The exact issue fixture is tested at the origin and with a translated board against the same flat/folded snapshots. Numeric checks cover CAD position/rotation, unchanged PCB records, and folded-to-flat restoration. Explicit RL/RR silkscreen labels make the expected behavior visible.
Validation: all 6 PCB-flex tests pass,
bunx tsc --noEmitpasses, andbun run buildpasses against the released packages. Existing flex snapshots still match. Temporarily restoring exporter 0.0.133 fails the new folded snapshot; 0.0.137 passes it.Flat reference:
Only the left tail folds:
Addresses item 1 of tscircuit/tscircuit#5277 and consumes tscircuit/flex-utils#5. Related: #4279 adds reported-error fallback for unsupported bend geometry and CAD mounts inside bend zones.