From 919803b1e1f63c03c20ef3d9edc37d2d5d04237a Mon Sep 17 00:00:00 2001 From: hrithik18k Date: Thu, 3 Sep 2026 14:33:32 +0530 Subject: [PATCH] fix(schematic): normalize wrapped arc sweeps --- lib/geometry/altium-geometry.ts | 7 ++ .../approximate-altium-arc.ts | 33 ++++++++ .../serialize-altium-sheet-to-svg.ts | 33 +++----- .../magnetic-core-inductor-schematic.snap.svg | 8 +- ...s62levm-schematic-sheets-sheet-13.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-20.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-21.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-22.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-25.snap.svg | 4 +- ...s62levm-schematic-sheets-sheet-26.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-27.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-32.snap.svg | 78 +++++++++---------- ...s62levm-schematic-sheets-sheet-33.snap.svg | 78 +++++++++---------- ...s62levm-schematic-sheets-sheet-39.snap.svg | 4 +- ...s62levm-schematic-sheets-sheet-40.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-44.snap.svg | 2 +- ...s62levm-schematic-sheets-sheet-51.snap.svg | 4 +- tests/svg/approximate-altium-arc.test.ts | 31 ++++++++ 18 files changed, 178 insertions(+), 118 deletions(-) create mode 100644 lib/svg-serialization/approximate-altium-arc.ts create mode 100644 tests/svg/approximate-altium-arc.test.ts diff --git a/lib/geometry/altium-geometry.ts b/lib/geometry/altium-geometry.ts index b4af271a..ca022d53 100644 --- a/lib/geometry/altium-geometry.ts +++ b/lib/geometry/altium-geometry.ts @@ -39,6 +39,13 @@ export function normalizeAltiumAngle(angle: number): number { return Object.is(normalized, -0) ? 0 : normalized } +export function getCcwSweepDegrees( + startAngleDegrees: number, + endAngleDegrees: number, +): number { + return normalizeAltiumAngle(endAngleDegrees - startAngleDegrees) || 360 +} + export function altiumPointsEqual( left: AltiumPoint, right: AltiumPoint, diff --git a/lib/svg-serialization/approximate-altium-arc.ts b/lib/svg-serialization/approximate-altium-arc.ts new file mode 100644 index 00000000..b9e10e08 --- /dev/null +++ b/lib/svg-serialization/approximate-altium-arc.ts @@ -0,0 +1,33 @@ +import { getCcwSweepDegrees } from "../geometry/altium-geometry" +import type { SvgPoint } from "./svg-types" + +interface ApproximateAltiumArcOptions { + center: SvgPoint + radius: number + startAngleDegrees: number + endAngleDegrees: number +} + +/** + * Samples an Altium arc in its native counterclockwise direction. Altium + * angles wrap through zero, so an arc from 360° to 90° sweeps 90°, not -270°. + */ +export function approximateAltiumArc({ + center, + radius, + startAngleDegrees, + endAngleDegrees, +}: ApproximateAltiumArcOptions): SvgPoint[] { + const ccwSweepDegrees = getCcwSweepDegrees(startAngleDegrees, endAngleDegrees) + const segments = Math.max(8, Math.ceil(ccwSweepDegrees / 7.5)) + + return Array.from({ length: segments + 1 }, (_, index) => { + const angleDegrees = + startAngleDegrees + (ccwSweepDegrees * index) / segments + const radians = (angleDegrees * Math.PI) / 180 + return { + x: center.x + Math.cos(radians) * radius, + y: center.y + Math.sin(radians) * radius, + } + }) +} diff --git a/lib/svg-serialization/serialize-altium-sheet-to-svg.ts b/lib/svg-serialization/serialize-altium-sheet-to-svg.ts index 38a3e6db..e1e5ef50 100644 --- a/lib/svg-serialization/serialize-altium-sheet-to-svg.ts +++ b/lib/svg-serialization/serialize-altium-sheet-to-svg.ts @@ -12,6 +12,7 @@ import { getSchematicCoordinate, getSchematicIndexedPoints, } from "./altium-values" +import { approximateAltiumArc } from "./approximate-altium-arc" import { getSchematicFont } from "./get-schematic-font" import { getSchematicSheetSize } from "./get-schematic-sheet-size" import { renderAltiumNegatedText } from "./render-altium-negated-text" @@ -44,7 +45,6 @@ interface SchematicPinRenderContext { sheetRecord: AltiumSchSheetRecord | undefined viewport: SvgViewport } - export function serializeAltiumSheetToSvg( source: AltiumPcbDoc | AltiumSchDoc | AltiumLine[], options: AltiumSheetSvgOptions = {}, @@ -217,9 +217,16 @@ function renderSchematicRecord( if (kind === "11" || kind === "12") { const center = getSchematicLocation(record) const radius = getSchematicCoordinate(record, "RADIUS", 1) - const startAngle = Number(record.getCaseInsensitive("STARTANGLE") ?? 0) - const endAngle = Number(record.getCaseInsensitive("ENDANGLE") ?? 360) - const points = approximateSchematicArc(center, radius, startAngle, endAngle) + const startAngleDegrees = Number( + record.getCaseInsensitive("STARTANGLE") ?? 0, + ) + const endAngleDegrees = Number(record.getCaseInsensitive("ENDANGLE") ?? 360) + const points = approximateAltiumArc({ + center, + radius, + startAngleDegrees, + endAngleDegrees, + }) return `` } @@ -1002,21 +1009,3 @@ function getSchematicCornerIfPresent( y: getSchematicCoordinate(record, "CORNER.Y"), } } - -function approximateSchematicArc( - center: SvgPoint, - radius: number, - startAngle: number, - endAngle: number, -): SvgPoint[] { - const sweep = endAngle - startAngle || 360 - const segments = Math.max(8, Math.ceil(Math.abs(sweep) / 7.5)) - return Array.from({ length: segments + 1 }, (_, index) => { - const angle = startAngle + (sweep * index) / segments - const radians = (angle * Math.PI) / 180 - return { - x: center.x + Math.cos(radians) * radius, - y: center.y + Math.sin(radians) * radius, - } - }) -} diff --git a/tests/svg/__snapshots__/magnetic-core-inductor-schematic.snap.svg b/tests/svg/__snapshots__/magnetic-core-inductor-schematic.snap.svg index 932ffb01..70bc8af0 100644 --- a/tests/svg/__snapshots__/magnetic-core-inductor-schematic.snap.svg +++ b/tests/svg/__snapshots__/magnetic-core-inductor-schematic.snap.svg @@ -25,13 +25,13 @@ - + - + - + - + 4.7uH L1 3x3mm power inductor diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-13.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-13.snap.svg index 3ab9e928..e57fc4bf 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-13.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-13.snap.svg @@ -207,7 +207,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-20.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-20.snap.svg index 83e0b873..c5611b48 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-20.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-20.snap.svg @@ -296,7 +296,7 @@ - + BLM18KG260TZ1D FL6 26E diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-21.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-21.snap.svg index 256fed3a..0cc89918 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-21.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-21.snap.svg @@ -334,7 +334,7 @@ - + FL1 120E diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-22.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-22.snap.svg index 8148daaa..8064e6c1 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-22.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-22.snap.svg @@ -269,7 +269,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-25.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-25.snap.svg index 46c8991c..b242116a 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-25.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-25.snap.svg @@ -330,7 +330,7 @@ - + @@ -525,7 +525,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-26.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-26.snap.svg index 67de2cd7..29d52e23 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-26.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-26.snap.svg @@ -144,7 +144,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-27.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-27.snap.svg index 0376cfa3..4ef3cd58 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-27.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-27.snap.svg @@ -49,7 +49,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-32.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-32.snap.svg index c5f37ff1..7679b8c1 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-32.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-32.snap.svg @@ -309,7 +309,7 @@ - + @@ -608,7 +608,7 @@ - + FL7 120E @@ -992,7 +992,7 @@ - + @@ -1018,12 +1018,12 @@ - + - + - + @@ -1040,7 +1040,7 @@ - + @@ -1058,9 +1058,9 @@ - + - + @@ -1085,16 +1085,16 @@ - + - + - - + + @@ -1137,7 +1137,7 @@ - + @@ -1159,12 +1159,12 @@ - - + + - + @@ -1194,7 +1194,7 @@ - + @@ -1207,13 +1207,13 @@ - - - - - - - + + + + + + + @@ -1237,13 +1237,13 @@ - - - - - - - + + + + + + + @@ -1253,13 +1253,13 @@ - - - - - - - + + + + + + + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-33.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-33.snap.svg index 2fdc19b1..3546de89 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-33.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-33.snap.svg @@ -341,7 +341,7 @@ - + @@ -367,12 +367,12 @@ - + - + - + @@ -389,7 +389,7 @@ - + @@ -407,9 +407,9 @@ - + - + @@ -434,16 +434,16 @@ - + - + - - + + @@ -486,7 +486,7 @@ - + @@ -508,12 +508,12 @@ - - + + - + @@ -543,7 +543,7 @@ - + @@ -556,13 +556,13 @@ - - - - - - - + + + + + + + @@ -586,13 +586,13 @@ - - - - - - - + + + + + + + @@ -602,13 +602,13 @@ - - - - - - - + + + + + + + @@ -712,7 +712,7 @@ - + @@ -944,7 +944,7 @@ - + FL5 120E diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-39.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-39.snap.svg index bc36e558..b5f09aba 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-39.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-39.snap.svg @@ -208,7 +208,7 @@ - + @@ -734,7 +734,7 @@ - + FL2 120E diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-40.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-40.snap.svg index c6a86070..4c8a8739 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-40.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-40.snap.svg @@ -174,7 +174,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-44.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-44.snap.svg index 3d3f0871..875c2bdf 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-44.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-44.snap.svg @@ -145,7 +145,7 @@ - + diff --git a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-51.snap.svg b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-51.snap.svg index c48404fd..227e4b68 100644 --- a/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-51.snap.svg +++ b/tests/svg/__snapshots__/ti-tmds62levm-schematic-sheets-sheet-51.snap.svg @@ -270,7 +270,7 @@ - + FL4 120E @@ -333,7 +333,7 @@ - + FL3 120E diff --git a/tests/svg/approximate-altium-arc.test.ts b/tests/svg/approximate-altium-arc.test.ts new file mode 100644 index 00000000..fa1dd4c5 --- /dev/null +++ b/tests/svg/approximate-altium-arc.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "bun:test" +import { approximateAltiumArc } from "../../lib/svg-serialization/approximate-altium-arc" + +test("wraps Altium arcs counterclockwise through zero degrees", () => { + const points = approximateAltiumArc({ + center: { x: 10, y: 20 }, + radius: 5, + startAngleDegrees: 360, + endAngleDegrees: 90, + }) + + expect(points).toHaveLength(13) + expect(points[0]?.x).toBeCloseTo(15) + expect(points[0]?.y).toBeCloseTo(20) + expect(points.at(-1)?.x).toBeCloseTo(10) + expect(points.at(-1)?.y).toBeCloseTo(25) + expect(points.every(({ x, y }) => x >= 10 && y >= 20)).toBe(true) +}) + +test("preserves equal-angle Altium arcs as full circles", () => { + const points = approximateAltiumArc({ + center: { x: 0, y: 0 }, + radius: 5, + startAngleDegrees: 45, + endAngleDegrees: 45, + }) + + expect(points).toHaveLength(49) + expect(points.at(-1)?.x).toBeCloseTo(points[0]?.x ?? 0) + expect(points.at(-1)?.y).toBeCloseTo(points[0]?.y ?? 0) +})