Add PCB teardrop controls and saved path width interpolation - #869
Conversation
| test("trace paths preserve taper fields and convert widths to millimeters", () => { | ||
| const input: TraceProps = { | ||
| from: "U1.1", | ||
| to: "J1.1", | ||
| pcbPath: [taper, "J1.1"], | ||
| pcbPaths: [[taper, { x: 1, y: 0 }]], | ||
| } | ||
| const parsed = traceProps.parse(input) | ||
| const expected = { ...taper, startWidth: 0.6, endWidth: 0.2 } | ||
| expect(parsed.pcbPath).toEqual([expected, "J1.1"]) | ||
| expect(parsed.pcbPaths).toEqual([[expected, { x: 1, y: 0 }]]) | ||
| }) | ||
|
|
||
| test("both profiles allow narrowing, widening and constant width", () => { | ||
| for (const widthInterpolationMode of ["linear", "quadratic"] as const) { | ||
| for (const [startWidth, endWidth] of [ | ||
| [0.6, 0.2], | ||
| [0.2, 0.6], | ||
| [0.2, 0.2], | ||
| ]) { | ||
| const point = { x: 0, y: 0, startWidth, endWidth, widthInterpolationMode } | ||
| expect(pcbPathPoint.parse(point)).toEqual(point) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| test("ordinary points and vias keep their existing output without taper defaults", () => { | ||
| for (const point of [ | ||
| { x: 0, y: 0 }, | ||
| { x: 1, y: 0, via: true, toLayer: "bottom" as const }, | ||
| ]) { | ||
| expect(pcbPathPoint.parse(point)).toEqual(point) | ||
| } | ||
| }) | ||
|
|
||
| test("taper fields must be supplied together and cannot belong to a via", () => { | ||
| for (const field of ["startWidth", "endWidth", "widthInterpolationMode"]) { | ||
| expect( | ||
| pcbPathPoint.safeParse({ ...taper, [field]: undefined }).success, | ||
| ).toBe(false) | ||
| } | ||
| expect( | ||
| pcbPathPoint.safeParse({ ...taper, via: true, toLayer: "bottom" }).success, | ||
| ).toBe(false) | ||
| }) | ||
|
|
||
| test("widths must be positive and finite and profiles must be supported", () => { | ||
| for (const field of ["startWidth", "endWidth"]) { | ||
| for (const value of [0, -1, Infinity, NaN, "-1mm"]) { | ||
| expect(pcbPathPoint.safeParse({ ...taper, [field]: value }).success).toBe( | ||
| false, | ||
| ) | ||
| } | ||
| } | ||
| expect( | ||
| pcbPathPoint.safeParse({ ...taper, widthInterpolationMode: "unsupported" }) | ||
| .success, | ||
| ).toBe(false) | ||
| }) |
There was a problem hiding this comment.
This file contains 5 test(...) calls (lines 13, 26, 39, 48, 59), but the style guide states that a *.test.ts file may have AT MOST one test(...). The remaining tests should be split into multiple numbered files, e.g., pcb-path-taper1.test.ts, pcb-path-taper2.test.ts, pcb-path-taper3.test.ts, pcb-path-taper4.test.ts, and pcb-path-taper5.test.ts, each containing exactly one test(...) call.
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
| import { expect, test } from "bun:test" | ||
| import { traceProps, type TraceProps } from "lib/components/trace" | ||
|
|
||
| test("teardrop booleans preserve explicit endpoint overrides for all connection forms", () => { | ||
| for (const connection of [ | ||
| { from: "U1.1", to: "J1.1" }, | ||
| { path: ["U1.1", "J1.1"] }, | ||
| ]) { | ||
| for (const pcbTeardrops of [true, false, undefined]) { | ||
| for (const pcbTeardropStart of [true, false, undefined]) { | ||
| for (const pcbTeardropEnd of [true, false, undefined]) { | ||
| const input: TraceProps = { | ||
| ...connection, | ||
| pcbTeardrops, | ||
| pcbTeardropStart, | ||
| pcbTeardropEnd, | ||
| pcbPath: [ | ||
| { x: 0, y: 0 }, | ||
| { x: 1, y: 0 }, | ||
| ], | ||
| } | ||
| const parsed = traceProps.parse(input) | ||
| expect(parsed.pcbTeardrops).toBe(pcbTeardrops) | ||
| expect(parsed.pcbTeardropStart).toBe(pcbTeardropStart) | ||
| expect(parsed.pcbTeardropEnd).toBe(pcbTeardropEnd) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| test("teardrop props reject non-booleans and introduce no defaults", () => { | ||
| const connection = { from: "U1.1", to: "J1.1" } | ||
| const parsed = traceProps.parse(connection) | ||
| for (const prop of [ | ||
| "pcbTeardrops", | ||
| "pcbTeardropStart", | ||
| "pcbTeardropEnd", | ||
| ] as const) { | ||
| expect(parsed).not.toHaveProperty(prop) | ||
| for (const value of ["yes", 1, null]) { | ||
| expect( | ||
| traceProps.safeParse({ ...connection, [prop]: value }).success, | ||
| ).toBe(false) | ||
| } | ||
| } | ||
| }) |
There was a problem hiding this comment.
This file contains two test(...) calls (one at line 4 and another at line 32), which violates the rule that a *.test.ts file may have AT MOST one test(...). Please split this into two separate numbered files, e.g. trace-teardrops1.test.ts (containing the 'teardrop booleans preserve explicit endpoint overrides for all connection forms' test) and trace-teardrops2.test.ts (containing the 'teardrop props reject non-booleans and introduce no defaults' test).
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
| test("saved route interpolation retains modes and normalizes point widths", () => { | ||
| for (const schema of [breakoutProps, autoroutingPhaseProps]) { | ||
| for (const mode of ["linear", "quadratic"] as const) { | ||
| const path: FanoutTracePath = { | ||
| connection: "U1.1", | ||
| route: [ | ||
| { ...wire, width: "800um", width_interpolation_mode: mode }, | ||
| { ...end, width: "0.2mm", width_interpolation_mode: mode }, | ||
| { ...wire, x: 2 }, | ||
| ], | ||
| } | ||
| const parsed = schema.parse({ pcbTracePaths: [path] }) | ||
| expect(parsed.pcbTracePaths?.[0]?.route).toEqual([ | ||
| { ...wire, width_interpolation_mode: mode }, | ||
| { ...end, width_interpolation_mode: mode }, | ||
| { ...wire, x: 2 }, | ||
| ]) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| test("ordinary differing widths remain unchanged and interpolation can end at a via contact", () => { | ||
| const route = [wire, end] | ||
| expect(fanoutTracePath.parse({ connection: "U1.1", route }).route).toEqual( | ||
| route, | ||
| ) | ||
| const input = { | ||
| connection: "U1.1", | ||
| route: [ | ||
| { ...wire, width_interpolation_mode: "quadratic" }, | ||
| end, | ||
| { route_type: "via", x: 1, y: 0, from_layer: "top", to_layer: "bottom" }, | ||
| ], | ||
| } as const | ||
| expect(fanoutTracePath.safeParse(input).success).toBe(true) | ||
| }) | ||
|
|
||
| test("interpolation rejects terminal, coincident, via and cross-layer endpoints", () => { | ||
| const start = { ...wire, width_interpolation_mode: "quadratic" } | ||
| for (const route of [ | ||
| [wire, { ...end, width_interpolation_mode: "quadratic" }], | ||
| [start, { ...end, x: 0 }], | ||
| [start, { ...end, layer: "bottom" }], | ||
| [ | ||
| start, | ||
| { route_type: "via", x: 1, y: 0, from_layer: "top", to_layer: "bottom" }, | ||
| ], | ||
| [{ ...start, width_interpolation_mode: "unsupported" }, end], | ||
| ]) { | ||
| expect( | ||
| fanoutTracePath.safeParse({ connection: "U1.1", route }).success, | ||
| ).toBe(false) | ||
| } | ||
| }) |
There was a problem hiding this comment.
This file contains three test(...) calls (at lines 18, 39, and 55), but the rule states that a *.test.ts file may have AT MOST one test(...). Please split this into multiple numbered files, e.g. fanout-trace-path-interpolation1.test.ts, fanout-trace-path-interpolation2.test.ts, and fanout-trace-path-interpolation3.test.ts.
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 |
Adds trace-level teardrop controls and explicit width interpolation for saved
pcbTracePaths.Trace controls
Optional booleans
pcbTeardrops,pcbTeardropStart, andpcbTeardropEndrequest automatic teardrops. Explicit endpoint values override the trace-wide setting. Omitted endpoint values inherit it; omittedpcbTeardropsis disabled. No defaults are inserted by parsing and no curve shape is prescribed by these controls.Saved route geometry
Wire points in
pcbTracePathsacceptwidth_interpolation_mode: "linear" | "quadratic". Each point retains its existingwidth; the mode controls interpolation to the next point's width. This is shared by fanout/breakout and autorouting-phase saved paths.Interpolation requires a distinct next wire point on the same layer. To terminate at a via, put the endpoint wire at the via coordinates. Terminal, coincident, direct-to-via, and cross-layer interpolation are rejected. Omitting the mode retains existing constant-width segment behavior, including paths with differing point widths. Distances still accept units and normalize to millimeters. No aliases or migration are required;
pcbPathis unchanged.Core implementation: tscircuit/core#4162 includes saved-route interpolation and automatic trace teardrops in a post-routing render phase.
Validation: 567 tests, TypeScript, and build pass. All four required generation scripts ran. README unchanged.