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 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
- getPcbLayerStack,
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 AltiumPadRecord && options.includePads !== false) {
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 AltiumTrackRecord) {
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 AltiumViaRecord && options.includeVias !== false) {
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 AltiumArcRecord) {
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 AltiumTrackRecord || record instanceof AltiumArcRecord ? record.widthMils ?? 4 : 0
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 = removeClosingPoint(path.points).map(toMillimeterPoint);
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 AltiumTrackRecord) {
343
+ if (record instanceof AltiumTrackRecord2) {
137
344
  return record.start && record.end ? [record.start, record.end] : [];
138
345
  }
139
- if (record instanceof AltiumArcRecord) {
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 AltiumRegionRecord) {
148
- return getPcbRegionGeometry(record).outline.points;
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 remaining = [...group];
168
- const stitched = [];
169
- while (remaining.length > 0) {
170
- const first = remaining.shift();
171
- if (!first) break;
172
- const path = { ...first, points: [...first.points] };
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 removeClosingPoint(points) {
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 outline = document.boardGeometry.outline.points.map(toMillimeterPoint);
238
- const bounds = getAltiumBounds(document.boardGeometry.outline.points) ?? getFallbackPcbBounds(document.records);
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 AltiumPadRecord || record instanceof AltiumViaRecord) {
444
+ if (record instanceof AltiumPadRecord2 || record instanceof AltiumViaRecord2) {
266
445
  if (record.position) points.push(record.position);
267
- } else if (record instanceof AltiumTrackRecord) {
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 getAltiumBounds(points) ?? { minX: 0, minY: 0, maxX: 1e3, maxY: 800 };
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 = normalizeLayer(layer);
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 = normalizeLayer(layer);
775
+ const normalized = normalizeLayer2(layer);
597
776
  return normalized === "TOPOVERLAY" || normalized === "BOTTOMOVERLAY";
598
777
  }
599
778
  function isCourtyardLayer(layer) {
600
- const normalized = normalizeLayer(layer);
779
+ const normalized = normalizeLayer2(layer);
601
780
  return normalized === "MECHANICAL15" || normalized === "MECHANICAL16";
602
781
  }
603
782
  function mapCourtyardLayer(layer) {
604
- return normalizeLayer(layer) === "MECHANICAL16" ? "bottom" : "top";
783
+ return normalizeLayer2(layer) === "MECHANICAL16" ? "bottom" : "top";
605
784
  }
606
785
  function mapOverlayLayer(layer) {
607
- return normalizeLayer(layer) === "BOTTOMOVERLAY" ? "bottom" : "top";
786
+ return normalizeLayer2(layer) === "BOTTOMOVERLAY" ? "bottom" : "top";
608
787
  }
609
788
  function getLayer(record) {
610
789
  return record.getDecoded("LAYER");
611
790
  }
612
- function normalizeLayer(layer) {
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 parsed = source_simple_resistor.safeParse({
1134
- ...common,
1135
- display_resistance: value || void 0,
1136
- ftype: "simple_resistor",
1137
- resistance: primaryValue || 0
1312
+ const resistance = parseFiniteComponentValue({
1313
+ componentUnit: "\u03A9",
1314
+ componentValue: primaryValue
1138
1315
  });
1139
- if (parsed.success) return parsed.data;
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 parsed = source_simple_capacitor.safeParse({
1143
- ...common,
1144
- display_capacitance: value || void 0,
1145
- ftype: "simple_capacitor",
1146
- capacitance: primaryValue || 0
1327
+ const capacitance = parseFiniteComponentValue({
1328
+ componentUnit: "F",
1329
+ componentValue: primaryValue
1147
1330
  });
1148
- if (parsed.success) return parsed.data;
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 parsed = source_simple_inductor.safeParse({
1152
- ...common,
1153
- display_inductance: value || void 0,
1154
- ftype: "simple_inductor",
1155
- inductance: primaryValue || 0
1342
+ const inductance = parseFiniteComponentValue({
1343
+ componentUnit: "H",
1344
+ componentValue: primaryValue
1156
1345
  });
1157
- if (parsed.success) return parsed.data;
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 = parseAndConvertSiUnit(value, "Hz").value;
1161
- if (typeof frequency === "number" && Number.isFinite(frequency)) {
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 {