altium-to-circuit-json 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +295 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,10 @@ bun run test:update-svg
|
|
|
94
94
|
|
|
95
95
|
Every imported reference used by the test suite has a side-by-side SVG snapshot. The committed fixtures are generated from pinned upstream URLs and are not stored in this repository.
|
|
96
96
|
|
|
97
|
+
The real-world PCB corpus covers NodeMCU ESP-12, EBAZ4205, HERON Payload SSM,
|
|
98
|
+
SimpleFOC Mini, and SimpleFOC Shield V3. The schematic corpus covers NodeMCU,
|
|
99
|
+
HERON PAY-SSM and Systems PCB, SimpleFOC Mini, and SimpleFOC Shield V3.
|
|
100
|
+
|
|
97
101
|
The TI SPRCAL9 / TMDS62LEVM Rev. B regression corpus includes all 57
|
|
98
102
|
schematic sheets and a top-copper PCB comparison. Its large downloaded source
|
|
99
103
|
files are checksum-verified and cached in CI. To also write the complete
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,224 @@
|
|
|
1
1
|
// lib/convert-altium-pcb-doc-to-circuit-json.ts
|
|
2
2
|
import {
|
|
3
|
-
AltiumArcRecord,
|
|
3
|
+
AltiumArcRecord as AltiumArcRecord2,
|
|
4
4
|
AltiumFillRecord,
|
|
5
|
+
AltiumPadRecord as AltiumPadRecord2,
|
|
6
|
+
AltiumRegionRecord as AltiumRegionRecord2,
|
|
7
|
+
AltiumTextRecord,
|
|
8
|
+
AltiumTrackRecord as AltiumTrackRecord2,
|
|
9
|
+
AltiumViaRecord as AltiumViaRecord2,
|
|
10
|
+
getAltiumBounds as getAltiumBounds2,
|
|
11
|
+
getPcbLayerStack,
|
|
12
|
+
getPcbRegionGeometry as getPcbRegionGeometry2,
|
|
13
|
+
parseAltiumMeasurementToMils
|
|
14
|
+
} from "altiumts";
|
|
15
|
+
|
|
16
|
+
// lib/pcb/get-board-outline.ts
|
|
17
|
+
import {
|
|
18
|
+
AltiumArcRecord,
|
|
5
19
|
AltiumPadRecord,
|
|
6
20
|
AltiumRegionRecord,
|
|
7
|
-
AltiumTextRecord,
|
|
8
21
|
AltiumTrackRecord,
|
|
9
22
|
AltiumViaRecord,
|
|
10
23
|
getAltiumBounds,
|
|
11
|
-
|
|
12
|
-
getPcbRegionGeometry,
|
|
13
|
-
parseAltiumMeasurementToMils
|
|
24
|
+
getPcbRegionGeometry
|
|
14
25
|
} from "altiumts";
|
|
26
|
+
|
|
27
|
+
// lib/pcb/stitch-connected-paths.ts
|
|
28
|
+
function stitchConnectedAltiumPaths({
|
|
29
|
+
paths,
|
|
30
|
+
maxEndpointGapMils
|
|
31
|
+
}) {
|
|
32
|
+
const remaining = paths.map((path) => [...path]);
|
|
33
|
+
const stitchedPaths = [];
|
|
34
|
+
while (remaining.length > 0) {
|
|
35
|
+
const path = remaining.shift();
|
|
36
|
+
if (!path) break;
|
|
37
|
+
while (appendConnectedPath({ path, remaining, maxEndpointGapMils })) {
|
|
38
|
+
}
|
|
39
|
+
stitchedPaths.push(path);
|
|
40
|
+
}
|
|
41
|
+
return stitchedPaths;
|
|
42
|
+
}
|
|
43
|
+
function appendConnectedPath({
|
|
44
|
+
path,
|
|
45
|
+
remaining,
|
|
46
|
+
maxEndpointGapMils
|
|
47
|
+
}) {
|
|
48
|
+
const pathStart = path[0];
|
|
49
|
+
const pathEnd = path.at(-1);
|
|
50
|
+
if (!pathStart || !pathEnd) return false;
|
|
51
|
+
for (const [index, candidate] of remaining.entries()) {
|
|
52
|
+
const candidateStart = candidate[0];
|
|
53
|
+
const candidateEnd = candidate.at(-1);
|
|
54
|
+
if (!candidateStart || !candidateEnd) continue;
|
|
55
|
+
if (pointsApproximatelyEqual({
|
|
56
|
+
left: pathEnd,
|
|
57
|
+
right: candidateStart,
|
|
58
|
+
maxEndpointGapMils
|
|
59
|
+
})) {
|
|
60
|
+
path.push(...candidate.slice(1));
|
|
61
|
+
} else if (pointsApproximatelyEqual({
|
|
62
|
+
left: pathEnd,
|
|
63
|
+
right: candidateEnd,
|
|
64
|
+
maxEndpointGapMils
|
|
65
|
+
})) {
|
|
66
|
+
path.push(...candidate.toReversed().slice(1));
|
|
67
|
+
} else if (pointsApproximatelyEqual({
|
|
68
|
+
left: pathStart,
|
|
69
|
+
right: candidateEnd,
|
|
70
|
+
maxEndpointGapMils
|
|
71
|
+
})) {
|
|
72
|
+
path.unshift(...candidate.slice(0, -1));
|
|
73
|
+
} else if (pointsApproximatelyEqual({
|
|
74
|
+
left: pathStart,
|
|
75
|
+
right: candidateStart,
|
|
76
|
+
maxEndpointGapMils
|
|
77
|
+
})) {
|
|
78
|
+
path.unshift(...candidate.toReversed().slice(0, -1));
|
|
79
|
+
} else {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
remaining.splice(index, 1);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function pointsApproximatelyEqual({
|
|
88
|
+
left,
|
|
89
|
+
right,
|
|
90
|
+
maxEndpointGapMils
|
|
91
|
+
}) {
|
|
92
|
+
return Math.abs(left.x - right.x) <= maxEndpointGapMils && Math.abs(left.y - right.y) <= maxEndpointGapMils;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// lib/pcb/get-board-outline.ts
|
|
96
|
+
var MAX_ENDPOINT_GAP_MILS = 5;
|
|
97
|
+
var MAX_PLACEMENT_OVERHANG_MILS = 100;
|
|
98
|
+
var MIN_STALE_OUTLINE_AREA_RATIO = 1.25;
|
|
99
|
+
function getPreferredPcbBoardOutline(document) {
|
|
100
|
+
const declaredOutline = document.boardGeometry.outline.points;
|
|
101
|
+
const keepoutOutline = getTightestEnclosingKeepoutOutline(document);
|
|
102
|
+
if (!keepoutOutline) return declaredOutline;
|
|
103
|
+
const declaredBounds = getAltiumBounds(declaredOutline);
|
|
104
|
+
const keepoutBounds = getAltiumBounds(keepoutOutline);
|
|
105
|
+
if (!keepoutBounds) return declaredOutline;
|
|
106
|
+
if (!declaredBounds || declaredOutline.length < 3) return keepoutOutline;
|
|
107
|
+
const declaredArea = getBoundsArea(declaredBounds);
|
|
108
|
+
const keepoutArea = getBoundsArea(keepoutBounds);
|
|
109
|
+
const declaredOutlineIsStale = declaredArea >= keepoutArea * MIN_STALE_OUTLINE_AREA_RATIO;
|
|
110
|
+
return declaredOutlineIsStale ? keepoutOutline : declaredOutline;
|
|
111
|
+
}
|
|
112
|
+
function getTightestEnclosingKeepoutOutline(document) {
|
|
113
|
+
const closedOutlines = [];
|
|
114
|
+
const openPaths = [];
|
|
115
|
+
for (const record of document.records) {
|
|
116
|
+
if (!isKeepoutLayer(record.getDecoded("LAYER"))) continue;
|
|
117
|
+
if (document.getComponentForRecord(record)) continue;
|
|
118
|
+
if (record instanceof AltiumRegionRecord) {
|
|
119
|
+
const outline = getPcbRegionGeometry(record).outline.points;
|
|
120
|
+
if (outline.length >= 3) closedOutlines.push(removeClosingPoint(outline));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (record instanceof AltiumTrackRecord && record.start && record.end) {
|
|
124
|
+
openPaths.push([record.start, record.end]);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (record instanceof AltiumArcRecord) {
|
|
128
|
+
const points = getArcPoints(record);
|
|
129
|
+
if (points.length >= 2) openPaths.push(points);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const path of stitchConnectedAltiumPaths({
|
|
133
|
+
paths: openPaths,
|
|
134
|
+
maxEndpointGapMils: MAX_ENDPOINT_GAP_MILS
|
|
135
|
+
})) {
|
|
136
|
+
if (!isClosedPath(path)) continue;
|
|
137
|
+
closedOutlines.push(removeClosingPoint(path));
|
|
138
|
+
}
|
|
139
|
+
const placedBounds = getPlacedContentBounds(document);
|
|
140
|
+
const candidates = closedOutlines.flatMap((outline) => {
|
|
141
|
+
const bounds = getAltiumBounds(outline);
|
|
142
|
+
if (!bounds || outline.length < 3) return [];
|
|
143
|
+
if (placedBounds && !boundsContainWithTolerance({
|
|
144
|
+
container: bounds,
|
|
145
|
+
contained: placedBounds,
|
|
146
|
+
tolerance: MAX_PLACEMENT_OVERHANG_MILS
|
|
147
|
+
})) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return [{ bounds, outline }];
|
|
151
|
+
});
|
|
152
|
+
candidates.sort((left, right) => {
|
|
153
|
+
const areaDifference = getBoundsArea(left.bounds) - getBoundsArea(right.bounds);
|
|
154
|
+
return placedBounds ? areaDifference : -areaDifference;
|
|
155
|
+
});
|
|
156
|
+
return candidates[0]?.outline;
|
|
157
|
+
}
|
|
158
|
+
function getPlacedContentBounds(document) {
|
|
159
|
+
const points = [];
|
|
160
|
+
for (const record of document.records) {
|
|
161
|
+
if (record instanceof AltiumPadRecord || record instanceof AltiumViaRecord) {
|
|
162
|
+
if (record.position) points.push(record.position);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (record instanceof AltiumTrackRecord && isCopperLayer(record.layer) && record.start && record.end) {
|
|
166
|
+
points.push(record.start, record.end);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return getAltiumBounds(points);
|
|
170
|
+
}
|
|
171
|
+
function getArcPoints(record) {
|
|
172
|
+
const center = record.center;
|
|
173
|
+
const radius = record.radiusMils;
|
|
174
|
+
if (!center || !radius) return [];
|
|
175
|
+
const rawSweep = record.endAngle - record.startAngle;
|
|
176
|
+
const sweep = rawSweep === 0 || Math.abs(rawSweep) >= 360 ? 360 : (rawSweep % 360 + 360) % 360;
|
|
177
|
+
const segmentCount = Math.max(8, Math.ceil(sweep / 7.5));
|
|
178
|
+
return Array.from({ length: segmentCount + 1 }, (_, index) => {
|
|
179
|
+
const angle = record.startAngle + sweep * index / segmentCount;
|
|
180
|
+
const radians = angle * Math.PI / 180;
|
|
181
|
+
return {
|
|
182
|
+
x: center.x + Math.cos(radians) * radius,
|
|
183
|
+
y: center.y + Math.sin(radians) * radius
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function boundsContainWithTolerance({
|
|
188
|
+
container,
|
|
189
|
+
contained,
|
|
190
|
+
tolerance
|
|
191
|
+
}) {
|
|
192
|
+
return contained.minX >= container.minX - tolerance && contained.maxX <= container.maxX + tolerance && contained.minY >= container.minY - tolerance && contained.maxY <= container.maxY + tolerance;
|
|
193
|
+
}
|
|
194
|
+
function getBoundsArea(bounds) {
|
|
195
|
+
return (bounds.maxX - bounds.minX) * (bounds.maxY - bounds.minY);
|
|
196
|
+
}
|
|
197
|
+
function isClosedPath(points) {
|
|
198
|
+
const first = points[0];
|
|
199
|
+
const last = points.at(-1);
|
|
200
|
+
return Boolean(first && last && pointsApproximatelyEqual2(first, last));
|
|
201
|
+
}
|
|
202
|
+
function removeClosingPoint(points) {
|
|
203
|
+
const first = points[0];
|
|
204
|
+
const last = points.at(-1);
|
|
205
|
+
return first && last && pointsApproximatelyEqual2(first, last) ? points.slice(0, -1) : points;
|
|
206
|
+
}
|
|
207
|
+
function pointsApproximatelyEqual2(left, right) {
|
|
208
|
+
return Math.abs(left.x - right.x) <= MAX_ENDPOINT_GAP_MILS && Math.abs(left.y - right.y) <= MAX_ENDPOINT_GAP_MILS;
|
|
209
|
+
}
|
|
210
|
+
function isKeepoutLayer(layer) {
|
|
211
|
+
return normalizeLayer(layer) === "KEEPOUT";
|
|
212
|
+
}
|
|
213
|
+
function isCopperLayer(layer) {
|
|
214
|
+
const normalized = normalizeLayer(layer);
|
|
215
|
+
return normalized === "TOP" || normalized === "TOPLAYER" || normalized === "BOTTOM" || normalized === "BOTTOMLAYER" || /^(?:MID|MIDLAYER|INTERNALPLANE)\d+$/u.test(normalized);
|
|
216
|
+
}
|
|
217
|
+
function normalizeLayer(layer) {
|
|
218
|
+
return (layer ?? "").replace(/[\s_.-]+/gu, "").toUpperCase();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// lib/convert-altium-pcb-doc-to-circuit-json.ts
|
|
15
222
|
var MILS_TO_MILLIMETERS = 0.0254;
|
|
16
223
|
var BOARD_ID = "pcb_board_altium";
|
|
17
224
|
var BOARD_GRAPHICS_COMPONENT_ID = "pcb_component_altium_board_graphics";
|
|
@@ -57,12 +264,12 @@ function convertAltiumPcbDocToCircuitJson(document, options = {}) {
|
|
|
57
264
|
elements.push(...convertCourtyards(document));
|
|
58
265
|
}
|
|
59
266
|
for (const [index, record] of document.records.entries()) {
|
|
60
|
-
if (record instanceof
|
|
267
|
+
if (record instanceof AltiumPadRecord2 && options.includePads !== false) {
|
|
61
268
|
const pad = convertPad(record, index);
|
|
62
269
|
if (pad) elements.push(pad);
|
|
63
270
|
continue;
|
|
64
271
|
}
|
|
65
|
-
if (record instanceof
|
|
272
|
+
if (record instanceof AltiumTrackRecord2) {
|
|
66
273
|
if (isCourtyardLayer(record.layer)) continue;
|
|
67
274
|
if (isOverlayLayer(record.layer)) {
|
|
68
275
|
if (options.includeSilkscreen === false) continue;
|
|
@@ -74,7 +281,7 @@ function convertAltiumPcbDocToCircuitJson(document, options = {}) {
|
|
|
74
281
|
}
|
|
75
282
|
continue;
|
|
76
283
|
}
|
|
77
|
-
if (record instanceof
|
|
284
|
+
if (record instanceof AltiumViaRecord2 && options.includeVias !== false) {
|
|
78
285
|
const via = convertVia(record, index);
|
|
79
286
|
if (via) elements.push(via);
|
|
80
287
|
continue;
|
|
@@ -82,7 +289,7 @@ function convertAltiumPcbDocToCircuitJson(document, options = {}) {
|
|
|
82
289
|
if (options.includeSilkscreen === false || !isOverlayLayer(getLayer(record))) {
|
|
83
290
|
continue;
|
|
84
291
|
}
|
|
85
|
-
if (record instanceof
|
|
292
|
+
if (record instanceof AltiumArcRecord2) {
|
|
86
293
|
const path = convertSilkscreenArc(record, index);
|
|
87
294
|
if (path) elements.push(path);
|
|
88
295
|
} else if (record instanceof AltiumFillRecord) {
|
|
@@ -114,13 +321,13 @@ function convertCourtyards(document) {
|
|
|
114
321
|
componentId: ownedComponentId,
|
|
115
322
|
layer: mapCourtyardLayer(layer),
|
|
116
323
|
points,
|
|
117
|
-
strokeWidthMils: record instanceof
|
|
324
|
+
strokeWidthMils: record instanceof AltiumTrackRecord2 || record instanceof AltiumArcRecord2 ? record.widthMils ?? 4 : 0
|
|
118
325
|
});
|
|
119
326
|
}
|
|
120
327
|
const courtyards = [];
|
|
121
328
|
for (const path of stitchCourtyardPaths(paths)) {
|
|
122
329
|
if (!isClosedAltiumPath(path.points)) continue;
|
|
123
|
-
const outline =
|
|
330
|
+
const outline = removeClosingPoint2(path.points).map(toMillimeterPoint);
|
|
124
331
|
if (outline.length < 3) continue;
|
|
125
332
|
courtyards.push({
|
|
126
333
|
type: "pcb_courtyard_outline",
|
|
@@ -133,10 +340,10 @@ function convertCourtyards(document) {
|
|
|
133
340
|
return courtyards;
|
|
134
341
|
}
|
|
135
342
|
function getCourtyardRecordPoints(record) {
|
|
136
|
-
if (record instanceof
|
|
343
|
+
if (record instanceof AltiumTrackRecord2) {
|
|
137
344
|
return record.start && record.end ? [record.start, record.end] : [];
|
|
138
345
|
}
|
|
139
|
-
if (record instanceof
|
|
346
|
+
if (record instanceof AltiumArcRecord2) {
|
|
140
347
|
return record.center && record.radiusMils ? approximateArc({
|
|
141
348
|
center: record.center,
|
|
142
349
|
radius: record.radiusMils,
|
|
@@ -144,8 +351,8 @@ function getCourtyardRecordPoints(record) {
|
|
|
144
351
|
endAngle: record.endAngle
|
|
145
352
|
}) : [];
|
|
146
353
|
}
|
|
147
|
-
if (record instanceof
|
|
148
|
-
return
|
|
354
|
+
if (record instanceof AltiumRegionRecord2) {
|
|
355
|
+
return getPcbRegionGeometry2(record).outline.points;
|
|
149
356
|
}
|
|
150
357
|
return [];
|
|
151
358
|
}
|
|
@@ -164,41 +371,12 @@ function stitchCourtyardPaths(paths) {
|
|
|
164
371
|
return [...groups.values()].flatMap(stitchCourtyardPathGroup);
|
|
165
372
|
}
|
|
166
373
|
function stitchCourtyardPathGroup(group) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
while (appendConnectedCourtyardPath(path, remaining)) {
|
|
174
|
-
}
|
|
175
|
-
stitched.push(path);
|
|
176
|
-
}
|
|
177
|
-
return stitched;
|
|
178
|
-
}
|
|
179
|
-
function appendConnectedCourtyardPath(stitched, remaining) {
|
|
180
|
-
const stitchedStart = stitched.points[0];
|
|
181
|
-
const stitchedEnd = stitched.points.at(-1);
|
|
182
|
-
if (!stitchedStart || !stitchedEnd) return false;
|
|
183
|
-
for (const [index, candidate] of remaining.entries()) {
|
|
184
|
-
const candidateStart = candidate.points[0];
|
|
185
|
-
const candidateEnd = candidate.points.at(-1);
|
|
186
|
-
if (!candidateStart || !candidateEnd) continue;
|
|
187
|
-
if (altiumPointsApproximatelyEqual(stitchedEnd, candidateStart)) {
|
|
188
|
-
stitched.points.push(...candidate.points.slice(1));
|
|
189
|
-
} else if (altiumPointsApproximatelyEqual(stitchedEnd, candidateEnd)) {
|
|
190
|
-
stitched.points.push(...candidate.points.toReversed().slice(1));
|
|
191
|
-
} else if (altiumPointsApproximatelyEqual(stitchedStart, candidateEnd)) {
|
|
192
|
-
stitched.points.unshift(...candidate.points.slice(0, -1));
|
|
193
|
-
} else if (altiumPointsApproximatelyEqual(stitchedStart, candidateStart)) {
|
|
194
|
-
stitched.points.unshift(...candidate.points.toReversed().slice(0, -1));
|
|
195
|
-
} else {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
remaining.splice(index, 1);
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
return false;
|
|
374
|
+
const first = group[0];
|
|
375
|
+
if (!first) return [];
|
|
376
|
+
return stitchConnectedAltiumPaths({
|
|
377
|
+
paths: group.map((path) => path.points),
|
|
378
|
+
maxEndpointGapMils: 0.01
|
|
379
|
+
}).map((points) => ({ ...first, points }));
|
|
202
380
|
}
|
|
203
381
|
function deduplicateCourtyardPaths(paths) {
|
|
204
382
|
const signatures = /* @__PURE__ */ new Set();
|
|
@@ -225,7 +403,7 @@ function isClosedAltiumPath(points) {
|
|
|
225
403
|
const last = points.at(-1);
|
|
226
404
|
return Boolean(first && last && altiumPointsApproximatelyEqual(first, last));
|
|
227
405
|
}
|
|
228
|
-
function
|
|
406
|
+
function removeClosingPoint2(points) {
|
|
229
407
|
const first = points[0];
|
|
230
408
|
const last = points.at(-1);
|
|
231
409
|
return first && last && altiumPointsApproximatelyEqual(first, last) ? points.slice(0, -1) : points;
|
|
@@ -234,8 +412,9 @@ function altiumPointsApproximatelyEqual(left, right) {
|
|
|
234
412
|
return Math.abs(left.x - right.x) <= 0.01 && Math.abs(left.y - right.y) <= 0.01;
|
|
235
413
|
}
|
|
236
414
|
function createBoard(document) {
|
|
237
|
-
const
|
|
238
|
-
const
|
|
415
|
+
const altiumOutline = getPreferredPcbBoardOutline(document);
|
|
416
|
+
const outline = altiumOutline.map(toMillimeterPoint);
|
|
417
|
+
const bounds = getAltiumBounds2(altiumOutline) ?? getFallbackPcbBounds(document.records);
|
|
239
418
|
const width = Math.max(milsToMillimeters(bounds.maxX - bounds.minX), 0.1);
|
|
240
419
|
const height = Math.max(milsToMillimeters(bounds.maxY - bounds.minY), 0.1);
|
|
241
420
|
const numLayers = document.board ? Math.max(
|
|
@@ -262,14 +441,14 @@ function createBoard(document) {
|
|
|
262
441
|
function getFallbackPcbBounds(records) {
|
|
263
442
|
const points = [];
|
|
264
443
|
for (const record of records) {
|
|
265
|
-
if (record instanceof
|
|
444
|
+
if (record instanceof AltiumPadRecord2 || record instanceof AltiumViaRecord2) {
|
|
266
445
|
if (record.position) points.push(record.position);
|
|
267
|
-
} else if (record instanceof
|
|
446
|
+
} else if (record instanceof AltiumTrackRecord2) {
|
|
268
447
|
if (record.start) points.push(record.start);
|
|
269
448
|
if (record.end) points.push(record.end);
|
|
270
449
|
}
|
|
271
450
|
}
|
|
272
|
-
return
|
|
451
|
+
return getAltiumBounds2(points) ?? { minX: 0, minY: 0, maxX: 1e3, maxY: 800 };
|
|
273
452
|
}
|
|
274
453
|
function convertTrack(record, index) {
|
|
275
454
|
const start = record.start;
|
|
@@ -584,7 +763,7 @@ function mapTextAnchor(justification) {
|
|
|
584
763
|
return "center";
|
|
585
764
|
}
|
|
586
765
|
function mapCopperLayer(layer) {
|
|
587
|
-
const normalized =
|
|
766
|
+
const normalized = normalizeLayer2(layer);
|
|
588
767
|
if (normalized === "TOP" || normalized === "TOPLAYER") return "top";
|
|
589
768
|
if (normalized === "BOTTOM" || normalized === "BOTTOMLAYER") return "bottom";
|
|
590
769
|
const innerMatch = /^(?:MID|MIDLAYER|INTERNALPLANE)(\d+)$/u.exec(normalized);
|
|
@@ -593,23 +772,23 @@ function mapCopperLayer(layer) {
|
|
|
593
772
|
return `inner${innerNumber}`;
|
|
594
773
|
}
|
|
595
774
|
function isOverlayLayer(layer) {
|
|
596
|
-
const normalized =
|
|
775
|
+
const normalized = normalizeLayer2(layer);
|
|
597
776
|
return normalized === "TOPOVERLAY" || normalized === "BOTTOMOVERLAY";
|
|
598
777
|
}
|
|
599
778
|
function isCourtyardLayer(layer) {
|
|
600
|
-
const normalized =
|
|
779
|
+
const normalized = normalizeLayer2(layer);
|
|
601
780
|
return normalized === "MECHANICAL15" || normalized === "MECHANICAL16";
|
|
602
781
|
}
|
|
603
782
|
function mapCourtyardLayer(layer) {
|
|
604
|
-
return
|
|
783
|
+
return normalizeLayer2(layer) === "MECHANICAL16" ? "bottom" : "top";
|
|
605
784
|
}
|
|
606
785
|
function mapOverlayLayer(layer) {
|
|
607
|
-
return
|
|
786
|
+
return normalizeLayer2(layer) === "BOTTOMOVERLAY" ? "bottom" : "top";
|
|
608
787
|
}
|
|
609
788
|
function getLayer(record) {
|
|
610
789
|
return record.getDecoded("LAYER");
|
|
611
790
|
}
|
|
612
|
-
function
|
|
791
|
+
function normalizeLayer2(layer) {
|
|
613
792
|
return (layer ?? "").replace(/[\s_.-]+/gu, "").toUpperCase();
|
|
614
793
|
}
|
|
615
794
|
function normalizeShape(shape) {
|
|
@@ -1130,35 +1309,56 @@ function createSourceComponent(params) {
|
|
|
1130
1309
|
source_component_id: sourceComponentId
|
|
1131
1310
|
};
|
|
1132
1311
|
if (classification === "resistor") {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
ftype: "simple_resistor",
|
|
1137
|
-
resistance: primaryValue || 0
|
|
1312
|
+
const resistance = parseFiniteComponentValue({
|
|
1313
|
+
componentUnit: "\u03A9",
|
|
1314
|
+
componentValue: primaryValue
|
|
1138
1315
|
});
|
|
1139
|
-
if (
|
|
1316
|
+
if (resistance !== void 0) {
|
|
1317
|
+
const parsed = source_simple_resistor.safeParse({
|
|
1318
|
+
...common,
|
|
1319
|
+
display_resistance: value || void 0,
|
|
1320
|
+
ftype: "simple_resistor",
|
|
1321
|
+
resistance
|
|
1322
|
+
});
|
|
1323
|
+
if (parsed.success) return parsed.data;
|
|
1324
|
+
}
|
|
1140
1325
|
}
|
|
1141
1326
|
if (classification === "capacitor") {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
ftype: "simple_capacitor",
|
|
1146
|
-
capacitance: primaryValue || 0
|
|
1327
|
+
const capacitance = parseFiniteComponentValue({
|
|
1328
|
+
componentUnit: "F",
|
|
1329
|
+
componentValue: primaryValue
|
|
1147
1330
|
});
|
|
1148
|
-
if (
|
|
1331
|
+
if (capacitance !== void 0) {
|
|
1332
|
+
const parsed = source_simple_capacitor.safeParse({
|
|
1333
|
+
...common,
|
|
1334
|
+
display_capacitance: value || void 0,
|
|
1335
|
+
ftype: "simple_capacitor",
|
|
1336
|
+
capacitance
|
|
1337
|
+
});
|
|
1338
|
+
if (parsed.success) return parsed.data;
|
|
1339
|
+
}
|
|
1149
1340
|
}
|
|
1150
1341
|
if (classification === "inductor") {
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
ftype: "simple_inductor",
|
|
1155
|
-
inductance: primaryValue || 0
|
|
1342
|
+
const inductance = parseFiniteComponentValue({
|
|
1343
|
+
componentUnit: "H",
|
|
1344
|
+
componentValue: primaryValue
|
|
1156
1345
|
});
|
|
1157
|
-
if (
|
|
1346
|
+
if (inductance !== void 0) {
|
|
1347
|
+
const parsed = source_simple_inductor.safeParse({
|
|
1348
|
+
...common,
|
|
1349
|
+
display_inductance: value || void 0,
|
|
1350
|
+
ftype: "simple_inductor",
|
|
1351
|
+
inductance
|
|
1352
|
+
});
|
|
1353
|
+
if (parsed.success) return parsed.data;
|
|
1354
|
+
}
|
|
1158
1355
|
}
|
|
1159
1356
|
if (classification === "crystal" && (pinCount === 2 || pinCount === 4)) {
|
|
1160
|
-
const frequency =
|
|
1161
|
-
|
|
1357
|
+
const frequency = parseFiniteComponentValue({
|
|
1358
|
+
componentUnit: "Hz",
|
|
1359
|
+
componentValue: value
|
|
1360
|
+
});
|
|
1361
|
+
if (frequency !== void 0) {
|
|
1162
1362
|
const parsed = source_simple_crystal.safeParse({
|
|
1163
1363
|
...common,
|
|
1164
1364
|
frequency,
|
|
@@ -1182,6 +1382,19 @@ function createSourceComponent(params) {
|
|
|
1182
1382
|
ftype
|
|
1183
1383
|
};
|
|
1184
1384
|
}
|
|
1385
|
+
function parseFiniteComponentValue({
|
|
1386
|
+
componentUnit,
|
|
1387
|
+
componentValue
|
|
1388
|
+
}) {
|
|
1389
|
+
const parsedComponentValue = parseAndConvertSiUnit(
|
|
1390
|
+
componentValue,
|
|
1391
|
+
componentUnit
|
|
1392
|
+
).value;
|
|
1393
|
+
if (typeof parsedComponentValue !== "number" || !Number.isFinite(parsedComponentValue)) {
|
|
1394
|
+
return void 0;
|
|
1395
|
+
}
|
|
1396
|
+
return parsedComponentValue;
|
|
1397
|
+
}
|
|
1185
1398
|
function createComponentText(params) {
|
|
1186
1399
|
const { anchor, component, id, position, text } = params;
|
|
1187
1400
|
return {
|