altium-to-circuit-json 0.0.2
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/LICENSE +22 -0
- package/README.md +88 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +1174 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// lib/convert-altium-pcb-doc-to-circuit-json.ts
|
|
2
|
+
import {
|
|
3
|
+
AltiumArcRecord,
|
|
4
|
+
AltiumFillRecord,
|
|
5
|
+
AltiumPadRecord,
|
|
6
|
+
AltiumTextRecord,
|
|
7
|
+
AltiumTrackRecord,
|
|
8
|
+
AltiumViaRecord,
|
|
9
|
+
getAltiumBounds,
|
|
10
|
+
getPcbLayerStack,
|
|
11
|
+
parseAltiumMeasurementToMils
|
|
12
|
+
} from "altiumts";
|
|
13
|
+
var MILS_TO_MILLIMETERS = 0.0254;
|
|
14
|
+
var BOARD_ID = "pcb_board_altium";
|
|
15
|
+
var BOARD_GRAPHICS_COMPONENT_ID = "pcb_component_altium_board_graphics";
|
|
16
|
+
function convertAltiumPcbDocToCircuitJson(document, options = {}) {
|
|
17
|
+
const elements = [];
|
|
18
|
+
if (options.includeBoardOutline !== false) {
|
|
19
|
+
elements.push(createBoard(document));
|
|
20
|
+
for (const [index, cutout] of document.boardGeometry.cutouts.entries()) {
|
|
21
|
+
if (cutout.outline.points.length < 3) continue;
|
|
22
|
+
elements.push({
|
|
23
|
+
type: "pcb_cutout",
|
|
24
|
+
pcb_cutout_id: `pcb_cutout_altium_${index}`,
|
|
25
|
+
pcb_board_id: BOARD_ID,
|
|
26
|
+
shape: "polygon",
|
|
27
|
+
points: cutout.outline.points.map(toMillimeterPoint)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options.includeComponents !== false) {
|
|
32
|
+
for (const [index, component] of document.components.entries()) {
|
|
33
|
+
const position = component.position;
|
|
34
|
+
if (!position) continue;
|
|
35
|
+
const bounds = document.getComponentBounds(component);
|
|
36
|
+
elements.push({
|
|
37
|
+
type: "pcb_component",
|
|
38
|
+
pcb_component_id: componentId(index),
|
|
39
|
+
source_component_id: `source_component_altium_${index}`,
|
|
40
|
+
center: toMillimeterPoint(position),
|
|
41
|
+
width: milsToMillimeters(
|
|
42
|
+
bounds ? bounds.maxX - bounds.minX : component.heightMils ?? 20
|
|
43
|
+
),
|
|
44
|
+
height: milsToMillimeters(
|
|
45
|
+
bounds ? bounds.maxY - bounds.minY : component.heightMils ?? 20
|
|
46
|
+
),
|
|
47
|
+
layer: component.side === "bottom" ? "bottom" : "top",
|
|
48
|
+
rotation: component.rotation,
|
|
49
|
+
position_mode: "none",
|
|
50
|
+
obstructs_within_bounds: false
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [index, record] of document.records.entries()) {
|
|
55
|
+
if (record instanceof AltiumPadRecord && options.includePads !== false) {
|
|
56
|
+
const pad = convertPad(record, index);
|
|
57
|
+
if (pad) elements.push(pad);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (record instanceof AltiumTrackRecord) {
|
|
61
|
+
if (isOverlayLayer(record.layer)) {
|
|
62
|
+
if (options.includeSilkscreen === false) continue;
|
|
63
|
+
const line = convertSilkscreenLine(record, index);
|
|
64
|
+
if (line) elements.push(line);
|
|
65
|
+
} else if (options.includeTraces !== false) {
|
|
66
|
+
const trace = convertTrack(record, index);
|
|
67
|
+
if (trace) elements.push(trace);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (record instanceof AltiumViaRecord && options.includeVias !== false) {
|
|
72
|
+
const via = convertVia(record, index);
|
|
73
|
+
if (via) elements.push(via);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (options.includeSilkscreen === false || !isOverlayLayer(getLayer(record))) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (record instanceof AltiumArcRecord) {
|
|
80
|
+
const path = convertSilkscreenArc(record, index);
|
|
81
|
+
if (path) elements.push(path);
|
|
82
|
+
} else if (record instanceof AltiumFillRecord) {
|
|
83
|
+
const rect = convertSilkscreenFill(record, index);
|
|
84
|
+
if (rect) elements.push(rect);
|
|
85
|
+
} else if (record instanceof AltiumTextRecord) {
|
|
86
|
+
const text = convertSilkscreenText(record, index);
|
|
87
|
+
if (text) elements.push(text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return elements;
|
|
91
|
+
}
|
|
92
|
+
function createBoard(document) {
|
|
93
|
+
const outline = document.boardGeometry.outline.points.map(toMillimeterPoint);
|
|
94
|
+
const bounds = getAltiumBounds(document.boardGeometry.outline.points) ?? getFallbackPcbBounds(document.records);
|
|
95
|
+
const width = Math.max(milsToMillimeters(bounds.maxX - bounds.minX), 0.1);
|
|
96
|
+
const height = Math.max(milsToMillimeters(bounds.maxY - bounds.minY), 0.1);
|
|
97
|
+
const numLayers = document.board ? Math.max(
|
|
98
|
+
getPcbLayerStack(document.board).entries.filter(
|
|
99
|
+
(entry) => Boolean(mapCopperLayer(entry.name ?? entry.layerId))
|
|
100
|
+
).length,
|
|
101
|
+
2
|
|
102
|
+
) : 2;
|
|
103
|
+
return {
|
|
104
|
+
type: "pcb_board",
|
|
105
|
+
pcb_board_id: BOARD_ID,
|
|
106
|
+
center: {
|
|
107
|
+
x: milsToMillimeters((bounds.minX + bounds.maxX) / 2),
|
|
108
|
+
y: milsToMillimeters((bounds.minY + bounds.maxY) / 2)
|
|
109
|
+
},
|
|
110
|
+
width,
|
|
111
|
+
height,
|
|
112
|
+
...outline.length >= 3 ? { shape: "polygon", outline } : {},
|
|
113
|
+
thickness: 1.6,
|
|
114
|
+
num_layers: numLayers,
|
|
115
|
+
material: "fr4"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function getFallbackPcbBounds(records) {
|
|
119
|
+
const points = [];
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
if (record instanceof AltiumPadRecord || record instanceof AltiumViaRecord) {
|
|
122
|
+
if (record.position) points.push(record.position);
|
|
123
|
+
} else if (record instanceof AltiumTrackRecord) {
|
|
124
|
+
if (record.start) points.push(record.start);
|
|
125
|
+
if (record.end) points.push(record.end);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return getAltiumBounds(points) ?? { minX: 0, minY: 0, maxX: 1e3, maxY: 800 };
|
|
129
|
+
}
|
|
130
|
+
function convertTrack(record, index) {
|
|
131
|
+
const start = record.start;
|
|
132
|
+
const end = record.end;
|
|
133
|
+
const layer = mapCopperLayer(record.layer);
|
|
134
|
+
if (!start || !end || !layer) return void 0;
|
|
135
|
+
const width = milsToMillimeters(record.widthMils ?? 4);
|
|
136
|
+
return {
|
|
137
|
+
type: "pcb_trace",
|
|
138
|
+
pcb_trace_id: `pcb_trace_altium_${index}`,
|
|
139
|
+
should_round_corners: true,
|
|
140
|
+
route: [
|
|
141
|
+
{ route_type: "wire", ...toMillimeterPoint(start), width, layer },
|
|
142
|
+
{ route_type: "wire", ...toMillimeterPoint(end), width, layer }
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function convertVia(record, index) {
|
|
147
|
+
if (!record.position) return void 0;
|
|
148
|
+
const startLayer = mapCopperLayer(record.startLayer) ?? "top";
|
|
149
|
+
const endLayer = mapCopperLayer(record.endLayer) ?? "bottom";
|
|
150
|
+
const layers = startLayer === endLayer ? [startLayer] : [startLayer, endLayer];
|
|
151
|
+
const outerDiameter = milsToMillimeters(record.diameterMils ?? 20);
|
|
152
|
+
return {
|
|
153
|
+
type: "pcb_via",
|
|
154
|
+
pcb_via_id: `pcb_via_altium_${index}`,
|
|
155
|
+
...toMillimeterPoint(record.position),
|
|
156
|
+
outer_diameter: outerDiameter,
|
|
157
|
+
hole_diameter: milsToMillimeters(
|
|
158
|
+
record.holeSizeMils ?? (record.diameterMils ?? 20) * 0.45
|
|
159
|
+
),
|
|
160
|
+
layers,
|
|
161
|
+
is_tented: record.tentedTop === true && record.tentedBottom === true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function convertPad(record, index) {
|
|
165
|
+
const position = record.position;
|
|
166
|
+
const size = record.size;
|
|
167
|
+
if (!position || !size) return void 0;
|
|
168
|
+
const x = milsToMillimeters(position.x);
|
|
169
|
+
const y = milsToMillimeters(position.y);
|
|
170
|
+
const width = milsToMillimeters(size.width);
|
|
171
|
+
const height = milsToMillimeters(size.height);
|
|
172
|
+
const holeDiameter = milsToMillimeters(record.holeSizeMils ?? 0);
|
|
173
|
+
const shape = normalizeShape(record.shape);
|
|
174
|
+
const id = `altium_${index}`;
|
|
175
|
+
if (record.plated === false && holeDiameter > 0) {
|
|
176
|
+
return {
|
|
177
|
+
type: "pcb_hole",
|
|
178
|
+
pcb_hole_id: `pcb_hole_${id}`,
|
|
179
|
+
hole_shape: "circle",
|
|
180
|
+
hole_diameter: holeDiameter,
|
|
181
|
+
x,
|
|
182
|
+
y
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (record.behavior === "through-hole" || holeDiameter > 0) {
|
|
186
|
+
const slotLengthMils = getMeasurement(record, "SLOTLENGTH");
|
|
187
|
+
const holeWidthMils = record.holeWidthMils ?? record.holeSizeMils;
|
|
188
|
+
const isSlot = normalizeShape(record.holeType).includes("SLOT") || (slotLengthMils ?? 0) > (record.holeSizeMils ?? 0) || (holeWidthMils ?? 0) > (record.holeSizeMils ?? 0);
|
|
189
|
+
const layers = ["top", "bottom"];
|
|
190
|
+
if (isSlot) {
|
|
191
|
+
const holeWidth = milsToMillimeters(
|
|
192
|
+
Math.max(
|
|
193
|
+
slotLengthMils ?? holeWidthMils ?? record.holeSizeMils ?? 1,
|
|
194
|
+
1
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
const holeHeight = Math.max(holeDiameter, MILS_TO_MILLIMETERS);
|
|
198
|
+
if (isRectangularShape(shape)) {
|
|
199
|
+
const rotated = record.holeRotation !== 0 || record.rotation !== 0;
|
|
200
|
+
return {
|
|
201
|
+
type: "pcb_plated_hole",
|
|
202
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
203
|
+
shape: rotated ? "rotated_pill_hole_with_rect_pad" : "pill_hole_with_rect_pad",
|
|
204
|
+
hole_shape: rotated ? "rotated_pill" : "pill",
|
|
205
|
+
pad_shape: "rect",
|
|
206
|
+
hole_width: holeWidth,
|
|
207
|
+
hole_height: holeHeight,
|
|
208
|
+
...rotated ? { hole_ccw_rotation: record.holeRotation } : {},
|
|
209
|
+
rect_pad_width: width,
|
|
210
|
+
rect_pad_height: height,
|
|
211
|
+
...rotated ? { rect_ccw_rotation: record.rotation } : {},
|
|
212
|
+
hole_offset_x: 0,
|
|
213
|
+
hole_offset_y: 0,
|
|
214
|
+
x,
|
|
215
|
+
y,
|
|
216
|
+
layers
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
type: "pcb_plated_hole",
|
|
221
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
222
|
+
shape: "pill",
|
|
223
|
+
outer_width: width,
|
|
224
|
+
outer_height: height,
|
|
225
|
+
hole_width: holeWidth,
|
|
226
|
+
hole_height: holeHeight,
|
|
227
|
+
ccw_rotation: record.holeRotation || record.rotation,
|
|
228
|
+
x,
|
|
229
|
+
y,
|
|
230
|
+
layers
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (isRectangularShape(shape)) {
|
|
234
|
+
return {
|
|
235
|
+
type: "pcb_plated_hole",
|
|
236
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
237
|
+
shape: "circular_hole_with_rect_pad",
|
|
238
|
+
hole_shape: "circle",
|
|
239
|
+
pad_shape: "rect",
|
|
240
|
+
hole_diameter: Math.max(holeDiameter, MILS_TO_MILLIMETERS),
|
|
241
|
+
rect_pad_width: width,
|
|
242
|
+
rect_pad_height: height,
|
|
243
|
+
rect_border_radius: shape.includes("ROUNDRECT") ? Math.min(width, height) * 0.18 : 0,
|
|
244
|
+
rect_ccw_rotation: record.rotation,
|
|
245
|
+
hole_offset_x: 0,
|
|
246
|
+
hole_offset_y: 0,
|
|
247
|
+
x,
|
|
248
|
+
y,
|
|
249
|
+
layers
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (shape.includes("OCTAGON")) {
|
|
253
|
+
return {
|
|
254
|
+
type: "pcb_plated_hole",
|
|
255
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
256
|
+
shape: "hole_with_polygon_pad",
|
|
257
|
+
hole_shape: "circle",
|
|
258
|
+
hole_diameter: Math.max(holeDiameter, MILS_TO_MILLIMETERS),
|
|
259
|
+
pad_outline: createOctagonPoints(x, y, width, height, record.rotation),
|
|
260
|
+
hole_offset_x: 0,
|
|
261
|
+
hole_offset_y: 0,
|
|
262
|
+
x,
|
|
263
|
+
y,
|
|
264
|
+
layers
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (shape === "ROUND" || shape === "CIRCLE" || shape === "OVAL") {
|
|
268
|
+
if (Math.abs(width - height) >= 1e-4) {
|
|
269
|
+
return {
|
|
270
|
+
type: "pcb_plated_hole",
|
|
271
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
272
|
+
shape: "pill",
|
|
273
|
+
outer_width: width,
|
|
274
|
+
outer_height: height,
|
|
275
|
+
hole_width: Math.max(holeDiameter, MILS_TO_MILLIMETERS),
|
|
276
|
+
hole_height: Math.max(holeDiameter, MILS_TO_MILLIMETERS),
|
|
277
|
+
ccw_rotation: record.rotation,
|
|
278
|
+
x,
|
|
279
|
+
y,
|
|
280
|
+
layers
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
type: "pcb_plated_hole",
|
|
286
|
+
pcb_plated_hole_id: `pcb_plated_hole_${id}`,
|
|
287
|
+
shape: "circle",
|
|
288
|
+
outer_diameter: Math.max(width, height),
|
|
289
|
+
hole_diameter: Math.max(holeDiameter, MILS_TO_MILLIMETERS),
|
|
290
|
+
x,
|
|
291
|
+
y,
|
|
292
|
+
layers
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const layer = mapCopperLayer(record.layer);
|
|
296
|
+
if (!layer) return void 0;
|
|
297
|
+
const base = {
|
|
298
|
+
type: "pcb_smtpad",
|
|
299
|
+
pcb_smtpad_id: `pcb_smtpad_${id}`,
|
|
300
|
+
x,
|
|
301
|
+
y,
|
|
302
|
+
layer,
|
|
303
|
+
soldermask_margin: record.solderMaskExpansionMils === void 0 ? void 0 : milsToMillimeters(record.solderMaskExpansionMils)
|
|
304
|
+
};
|
|
305
|
+
if (shape.includes("OCTAGON")) {
|
|
306
|
+
return {
|
|
307
|
+
...base,
|
|
308
|
+
shape: "polygon",
|
|
309
|
+
points: createOctagonPoints(x, y, width, height, record.rotation)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (shape === "ROUND" || shape === "CIRCLE" || shape === "OVAL") {
|
|
313
|
+
if (Math.abs(width - height) < 1e-4) {
|
|
314
|
+
return { ...base, shape: "circle", radius: width / 2 };
|
|
315
|
+
}
|
|
316
|
+
return record.rotation === 0 ? {
|
|
317
|
+
...base,
|
|
318
|
+
shape: "pill",
|
|
319
|
+
width,
|
|
320
|
+
height,
|
|
321
|
+
radius: Math.min(width, height) / 2
|
|
322
|
+
} : {
|
|
323
|
+
...base,
|
|
324
|
+
shape: "rotated_pill",
|
|
325
|
+
width,
|
|
326
|
+
height,
|
|
327
|
+
radius: Math.min(width, height) / 2,
|
|
328
|
+
ccw_rotation: record.rotation
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const cornerRadius = shape.includes("ROUNDRECT") ? Math.min(width, height) * 0.18 : void 0;
|
|
332
|
+
return record.rotation === 0 ? { ...base, shape: "rect", width, height, corner_radius: cornerRadius } : {
|
|
333
|
+
...base,
|
|
334
|
+
shape: "rotated_rect",
|
|
335
|
+
width,
|
|
336
|
+
height,
|
|
337
|
+
corner_radius: cornerRadius,
|
|
338
|
+
ccw_rotation: record.rotation
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function convertSilkscreenLine(record, index) {
|
|
342
|
+
if (!record.start || !record.end) return void 0;
|
|
343
|
+
return {
|
|
344
|
+
type: "pcb_silkscreen_line",
|
|
345
|
+
pcb_silkscreen_line_id: `pcb_silkscreen_line_altium_${index}`,
|
|
346
|
+
pcb_component_id: pcbComponentIdForRecord(record),
|
|
347
|
+
stroke_width: milsToMillimeters(record.widthMils ?? 4),
|
|
348
|
+
...withNumberedPoints(record.start, record.end),
|
|
349
|
+
layer: mapOverlayLayer(record.layer)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function convertSilkscreenArc(record, index) {
|
|
353
|
+
if (!record.center || !record.radiusMils) return void 0;
|
|
354
|
+
const points = approximateArc(
|
|
355
|
+
record.center,
|
|
356
|
+
record.radiusMils,
|
|
357
|
+
record.startAngle,
|
|
358
|
+
record.endAngle
|
|
359
|
+
);
|
|
360
|
+
return {
|
|
361
|
+
type: "pcb_silkscreen_path",
|
|
362
|
+
pcb_silkscreen_path_id: `pcb_silkscreen_path_altium_${index}`,
|
|
363
|
+
pcb_component_id: pcbComponentIdForRecord(record),
|
|
364
|
+
route: points.map(toMillimeterPoint),
|
|
365
|
+
stroke_width: milsToMillimeters(record.widthMils ?? 4),
|
|
366
|
+
layer: mapOverlayLayer(record.layer)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function convertSilkscreenFill(record, index) {
|
|
370
|
+
if (!record.bounds) return void 0;
|
|
371
|
+
const width = milsToMillimeters(record.bounds.maxX - record.bounds.minX);
|
|
372
|
+
const height = milsToMillimeters(record.bounds.maxY - record.bounds.minY);
|
|
373
|
+
return {
|
|
374
|
+
type: "pcb_silkscreen_rect",
|
|
375
|
+
pcb_silkscreen_rect_id: `pcb_silkscreen_rect_altium_${index}`,
|
|
376
|
+
pcb_component_id: pcbComponentIdForRecord(record),
|
|
377
|
+
center: {
|
|
378
|
+
x: milsToMillimeters((record.bounds.minX + record.bounds.maxX) / 2),
|
|
379
|
+
y: milsToMillimeters((record.bounds.minY + record.bounds.maxY) / 2)
|
|
380
|
+
},
|
|
381
|
+
width,
|
|
382
|
+
height,
|
|
383
|
+
stroke_width: Math.min(width, height),
|
|
384
|
+
is_filled: true,
|
|
385
|
+
has_stroke: false,
|
|
386
|
+
ccw_rotation: record.rotation,
|
|
387
|
+
layer: mapOverlayLayer(record.layer)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function convertSilkscreenText(record, index) {
|
|
391
|
+
const text = decodeAltiumWideString(record.getDecoded("WIDESTRING")) || record.getDecoded("TEXT") || record.text;
|
|
392
|
+
if (!record.position || !text) return void 0;
|
|
393
|
+
return {
|
|
394
|
+
type: "pcb_silkscreen_text",
|
|
395
|
+
pcb_silkscreen_text_id: `pcb_silkscreen_text_altium_${index}`,
|
|
396
|
+
pcb_component_id: pcbComponentIdForRecord(record),
|
|
397
|
+
text,
|
|
398
|
+
font: "tscircuit2024",
|
|
399
|
+
font_size: milsToMillimeters(record.heightMils ?? 30),
|
|
400
|
+
anchor_position: toMillimeterPoint(record.position),
|
|
401
|
+
anchor_alignment: mapTextAnchor(record.justification),
|
|
402
|
+
ccw_rotation: record.rotation,
|
|
403
|
+
layer: mapOverlayLayer(record.layer),
|
|
404
|
+
is_mirrored: record.mirrored
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function decodeAltiumWideString(raw) {
|
|
408
|
+
if (!raw) return "";
|
|
409
|
+
if (!/^\d+(?:,\d+)*$/u.test(raw)) return raw;
|
|
410
|
+
try {
|
|
411
|
+
return String.fromCodePoint(...raw.split(",").map(Number));
|
|
412
|
+
} catch {
|
|
413
|
+
return raw;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function pcbComponentIdForRecord(record) {
|
|
417
|
+
const index = record.getNumber("COMPONENT");
|
|
418
|
+
return index === void 0 || index < 0 ? BOARD_GRAPHICS_COMPONENT_ID : componentId(index);
|
|
419
|
+
}
|
|
420
|
+
function componentId(index) {
|
|
421
|
+
return `pcb_component_altium_${index}`;
|
|
422
|
+
}
|
|
423
|
+
function mapTextAnchor(justification) {
|
|
424
|
+
const normalized = justification?.replace(/[\s_-]+/gu, "").toUpperCase();
|
|
425
|
+
if (normalized?.includes("CENTER")) return "bottom_center";
|
|
426
|
+
if (normalized?.includes("RIGHT")) return "bottom_right";
|
|
427
|
+
if (normalized?.includes("LEFT")) return "bottom_left";
|
|
428
|
+
return "center";
|
|
429
|
+
}
|
|
430
|
+
function mapCopperLayer(layer) {
|
|
431
|
+
const normalized = normalizeLayer(layer);
|
|
432
|
+
if (normalized === "TOP" || normalized === "TOPLAYER") return "top";
|
|
433
|
+
if (normalized === "BOTTOM" || normalized === "BOTTOMLAYER") return "bottom";
|
|
434
|
+
const innerMatch = /^(?:MID|MIDLAYER|INTERNALPLANE)(\d+)$/u.exec(normalized);
|
|
435
|
+
if (!innerMatch?.[1]) return void 0;
|
|
436
|
+
const innerNumber = Math.min(Math.max(Number(innerMatch[1]), 1), 8);
|
|
437
|
+
return `inner${innerNumber}`;
|
|
438
|
+
}
|
|
439
|
+
function isOverlayLayer(layer) {
|
|
440
|
+
const normalized = normalizeLayer(layer);
|
|
441
|
+
return normalized === "TOPOVERLAY" || normalized === "BOTTOMOVERLAY";
|
|
442
|
+
}
|
|
443
|
+
function mapOverlayLayer(layer) {
|
|
444
|
+
return normalizeLayer(layer) === "BOTTOMOVERLAY" ? "bottom" : "top";
|
|
445
|
+
}
|
|
446
|
+
function getLayer(record) {
|
|
447
|
+
return record.getDecoded("LAYER");
|
|
448
|
+
}
|
|
449
|
+
function normalizeLayer(layer) {
|
|
450
|
+
return (layer ?? "").replace(/[\s_.-]+/gu, "").toUpperCase();
|
|
451
|
+
}
|
|
452
|
+
function normalizeShape(shape) {
|
|
453
|
+
return (shape ?? "ROUND").replace(/[\s_-]+/gu, "").toUpperCase();
|
|
454
|
+
}
|
|
455
|
+
function isRectangularShape(shape) {
|
|
456
|
+
return shape.includes("RECT") || shape === "SQUARE";
|
|
457
|
+
}
|
|
458
|
+
function getMeasurement(record, key) {
|
|
459
|
+
return parseAltiumMeasurementToMils(record.getCaseInsensitive(key));
|
|
460
|
+
}
|
|
461
|
+
function milsToMillimeters(value) {
|
|
462
|
+
return value * MILS_TO_MILLIMETERS;
|
|
463
|
+
}
|
|
464
|
+
function toMillimeterPoint(point) {
|
|
465
|
+
return { x: milsToMillimeters(point.x), y: milsToMillimeters(point.y) };
|
|
466
|
+
}
|
|
467
|
+
function withNumberedPoints(start, end) {
|
|
468
|
+
return {
|
|
469
|
+
x1: milsToMillimeters(start.x),
|
|
470
|
+
y1: milsToMillimeters(start.y),
|
|
471
|
+
x2: milsToMillimeters(end.x),
|
|
472
|
+
y2: milsToMillimeters(end.y)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function approximateArc(center, radius, startAngle, endAngle) {
|
|
476
|
+
const sweep = endAngle - startAngle || 360;
|
|
477
|
+
const segments = Math.max(8, Math.ceil(Math.abs(sweep) / 7.5));
|
|
478
|
+
return Array.from({ length: segments + 1 }, (_, index) => {
|
|
479
|
+
const angle = startAngle + sweep * index / segments;
|
|
480
|
+
const radians = angle * Math.PI / 180;
|
|
481
|
+
return {
|
|
482
|
+
x: center.x + Math.cos(radians) * radius,
|
|
483
|
+
y: center.y + Math.sin(radians) * radius
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function createOctagonPoints(x, y, width, height, rotation) {
|
|
488
|
+
const halfWidth = width / 2;
|
|
489
|
+
const halfHeight = height / 2;
|
|
490
|
+
const chamfer = Math.min(width, height) / 4;
|
|
491
|
+
const points = [
|
|
492
|
+
{ x: -halfWidth + chamfer, y: -halfHeight },
|
|
493
|
+
{ x: halfWidth - chamfer, y: -halfHeight },
|
|
494
|
+
{ x: halfWidth, y: -halfHeight + chamfer },
|
|
495
|
+
{ x: halfWidth, y: halfHeight - chamfer },
|
|
496
|
+
{ x: halfWidth - chamfer, y: halfHeight },
|
|
497
|
+
{ x: -halfWidth + chamfer, y: halfHeight },
|
|
498
|
+
{ x: -halfWidth, y: halfHeight - chamfer },
|
|
499
|
+
{ x: -halfWidth, y: -halfHeight + chamfer }
|
|
500
|
+
];
|
|
501
|
+
const radians = rotation * Math.PI / 180;
|
|
502
|
+
return points.map((point) => ({
|
|
503
|
+
x: x + point.x * Math.cos(radians) - point.y * Math.sin(radians),
|
|
504
|
+
y: y + point.x * Math.sin(radians) + point.y * Math.cos(radians)
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// lib/convert-altium-sch-doc-to-circuit-json.ts
|
|
509
|
+
import {
|
|
510
|
+
getSchematicRecordPoints
|
|
511
|
+
} from "altiumts";
|
|
512
|
+
var DEFAULT_SCHEMATIC_UNIT_SCALE = 0.1;
|
|
513
|
+
var SCHEMATIC_SHEET_ID = "schematic_sheet_altium";
|
|
514
|
+
function convertAltiumSchDocToCircuitJson(document, options = {}) {
|
|
515
|
+
const scale = options.schematicUnitScale ?? DEFAULT_SCHEMATIC_UNIT_SCALE;
|
|
516
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
517
|
+
throw new RangeError("schematicUnitScale must be a positive finite number");
|
|
518
|
+
}
|
|
519
|
+
const records = document.records;
|
|
520
|
+
const sheetRecord = records.find((record) => record.recordKind === "31");
|
|
521
|
+
const context = { document, records, scale, sheetRecord };
|
|
522
|
+
const elements = [
|
|
523
|
+
{
|
|
524
|
+
type: "schematic_sheet",
|
|
525
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
526
|
+
name: options.sheetName ?? "Altium schematic",
|
|
527
|
+
sheet_index: 0
|
|
528
|
+
}
|
|
529
|
+
];
|
|
530
|
+
if (options.includeSheetBorder !== false) {
|
|
531
|
+
elements.push(createSheetBorder(sheetRecord, scale));
|
|
532
|
+
}
|
|
533
|
+
for (const [index, record] of records.entries()) {
|
|
534
|
+
if (!shouldRenderSchematicRecord(record, context)) continue;
|
|
535
|
+
const converted = convertSchematicRecord(record, index, context, options);
|
|
536
|
+
elements.push(...converted);
|
|
537
|
+
}
|
|
538
|
+
return elements;
|
|
539
|
+
}
|
|
540
|
+
function convertSchematicRecord(record, index, context, options) {
|
|
541
|
+
const kind = record.recordKind;
|
|
542
|
+
const scale = context.scale;
|
|
543
|
+
const color = altiumColorToCss(record.getCaseInsensitive("COLOR"), "#1f2937");
|
|
544
|
+
const strokeWidth = Math.max(
|
|
545
|
+
Number(record.getCaseInsensitive("LINEWIDTH") ?? 1) * scale,
|
|
546
|
+
0.05
|
|
547
|
+
);
|
|
548
|
+
if (kind === "27") {
|
|
549
|
+
const points = getSchematicRecordPoints(record).map(
|
|
550
|
+
(point) => scalePoint(point, scale)
|
|
551
|
+
);
|
|
552
|
+
if (points.length < 2) return [];
|
|
553
|
+
return [
|
|
554
|
+
{
|
|
555
|
+
type: "schematic_trace",
|
|
556
|
+
schematic_trace_id: `schematic_trace_altium_${index}`,
|
|
557
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
558
|
+
junctions: [],
|
|
559
|
+
edges: points.slice(1).map((point, pointIndex) => ({
|
|
560
|
+
from: points[pointIndex] ?? point,
|
|
561
|
+
to: point
|
|
562
|
+
}))
|
|
563
|
+
}
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
if (kind === "6" || kind === "7") {
|
|
567
|
+
const points = getSchematicRecordPoints(record).map(
|
|
568
|
+
(point) => scalePoint(point, scale)
|
|
569
|
+
);
|
|
570
|
+
if (points.length < 2) return [];
|
|
571
|
+
return [
|
|
572
|
+
{
|
|
573
|
+
type: "schematic_path",
|
|
574
|
+
schematic_path_id: `schematic_path_altium_${index}`,
|
|
575
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
576
|
+
points,
|
|
577
|
+
stroke_width: strokeWidth,
|
|
578
|
+
stroke_color: color,
|
|
579
|
+
fill_color: kind === "7" ? altiumColorToCss(
|
|
580
|
+
record.getCaseInsensitive("AREACOLOR"),
|
|
581
|
+
"transparent"
|
|
582
|
+
) : void 0,
|
|
583
|
+
is_filled: kind === "7",
|
|
584
|
+
is_dashed: false
|
|
585
|
+
}
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
if (kind === "13") {
|
|
589
|
+
const location = getLocation(record);
|
|
590
|
+
const corner = getCorner(record);
|
|
591
|
+
if (!location || !corner) return [];
|
|
592
|
+
return [createLine(index, location, corner, color, strokeWidth, scale)];
|
|
593
|
+
}
|
|
594
|
+
if (kind === "10" || kind === "14") {
|
|
595
|
+
const rectangle = getRectangle(record);
|
|
596
|
+
if (!rectangle) return [];
|
|
597
|
+
return [
|
|
598
|
+
{
|
|
599
|
+
type: "schematic_rect",
|
|
600
|
+
schematic_rect_id: `schematic_rect_altium_${index}`,
|
|
601
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
602
|
+
center: scalePoint(
|
|
603
|
+
{
|
|
604
|
+
x: (rectangle.minX + rectangle.maxX) / 2,
|
|
605
|
+
y: (rectangle.minY + rectangle.maxY) / 2
|
|
606
|
+
},
|
|
607
|
+
scale
|
|
608
|
+
),
|
|
609
|
+
width: (rectangle.maxX - rectangle.minX) * scale,
|
|
610
|
+
height: (rectangle.maxY - rectangle.minY) * scale,
|
|
611
|
+
rotation: 0,
|
|
612
|
+
stroke_width: strokeWidth,
|
|
613
|
+
color,
|
|
614
|
+
is_filled: record.getBoolean("ISSOLID") === true,
|
|
615
|
+
fill_color: altiumColorToCss(
|
|
616
|
+
record.getCaseInsensitive("AREACOLOR"),
|
|
617
|
+
"#ffffff"
|
|
618
|
+
),
|
|
619
|
+
is_dashed: false
|
|
620
|
+
}
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
if (kind === "8") {
|
|
624
|
+
const center = getLocation(record);
|
|
625
|
+
if (!center) return [];
|
|
626
|
+
const radiusX = getCoordinate(record, "RADIUS", 1);
|
|
627
|
+
const radiusY = getCoordinate(record, "SECONDARYRADIUS", radiusX);
|
|
628
|
+
if (Math.abs(radiusX - radiusY) < 1e-4) {
|
|
629
|
+
return [
|
|
630
|
+
{
|
|
631
|
+
type: "schematic_circle",
|
|
632
|
+
schematic_circle_id: `schematic_circle_altium_${index}`,
|
|
633
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
634
|
+
center: scalePoint(center, scale),
|
|
635
|
+
radius: radiusX * scale,
|
|
636
|
+
stroke_width: strokeWidth,
|
|
637
|
+
color,
|
|
638
|
+
is_filled: record.getBoolean("ISSOLID") === true,
|
|
639
|
+
fill_color: altiumColorToCss(
|
|
640
|
+
record.getCaseInsensitive("AREACOLOR"),
|
|
641
|
+
"#ffffff"
|
|
642
|
+
),
|
|
643
|
+
is_dashed: false
|
|
644
|
+
}
|
|
645
|
+
];
|
|
646
|
+
}
|
|
647
|
+
return [
|
|
648
|
+
{
|
|
649
|
+
type: "schematic_path",
|
|
650
|
+
schematic_path_id: `schematic_ellipse_altium_${index}`,
|
|
651
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
652
|
+
points: approximateEllipse(center, radiusX, radiusY).map(
|
|
653
|
+
(point) => scalePoint(point, scale)
|
|
654
|
+
),
|
|
655
|
+
stroke_width: strokeWidth,
|
|
656
|
+
stroke_color: color,
|
|
657
|
+
fill_color: altiumColorToCss(
|
|
658
|
+
record.getCaseInsensitive("AREACOLOR"),
|
|
659
|
+
"#ffffff"
|
|
660
|
+
),
|
|
661
|
+
is_filled: record.getBoolean("ISSOLID") === true,
|
|
662
|
+
is_dashed: false
|
|
663
|
+
}
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
if (kind === "11" || kind === "12") {
|
|
667
|
+
const center = getLocation(record);
|
|
668
|
+
if (!center) return [];
|
|
669
|
+
return [
|
|
670
|
+
{
|
|
671
|
+
type: "schematic_arc",
|
|
672
|
+
schematic_arc_id: `schematic_arc_altium_${index}`,
|
|
673
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
674
|
+
center: scalePoint(center, scale),
|
|
675
|
+
radius: getCoordinate(record, "RADIUS", 1) * scale,
|
|
676
|
+
start_angle_degrees: Number(
|
|
677
|
+
record.getCaseInsensitive("STARTANGLE") ?? 0
|
|
678
|
+
),
|
|
679
|
+
end_angle_degrees: Number(record.getCaseInsensitive("ENDANGLE") ?? 360),
|
|
680
|
+
direction: "counterclockwise",
|
|
681
|
+
stroke_width: strokeWidth,
|
|
682
|
+
color,
|
|
683
|
+
is_dashed: false
|
|
684
|
+
}
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
if (kind === "2") {
|
|
688
|
+
return convertPin(record, index, context, options, color);
|
|
689
|
+
}
|
|
690
|
+
if (kind === "29") {
|
|
691
|
+
const location = getLocation(record);
|
|
692
|
+
if (!location) return [];
|
|
693
|
+
return [
|
|
694
|
+
{
|
|
695
|
+
type: "schematic_circle",
|
|
696
|
+
schematic_circle_id: `schematic_junction_altium_${index}`,
|
|
697
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
698
|
+
center: scalePoint(location, scale),
|
|
699
|
+
radius: Math.max(
|
|
700
|
+
Number(record.getCaseInsensitive("SIZE") ?? 1) * 0.18,
|
|
701
|
+
0.15
|
|
702
|
+
),
|
|
703
|
+
color,
|
|
704
|
+
is_filled: true,
|
|
705
|
+
fill_color: color,
|
|
706
|
+
is_dashed: false
|
|
707
|
+
}
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
if (kind === "22") {
|
|
711
|
+
const location = getLocation(record);
|
|
712
|
+
if (!location) return [];
|
|
713
|
+
const radius = 0.4 / scale;
|
|
714
|
+
return [
|
|
715
|
+
createLine(
|
|
716
|
+
index,
|
|
717
|
+
{ x: location.x - radius, y: location.y - radius },
|
|
718
|
+
{ x: location.x + radius, y: location.y + radius },
|
|
719
|
+
color,
|
|
720
|
+
0.1,
|
|
721
|
+
scale,
|
|
722
|
+
"a"
|
|
723
|
+
),
|
|
724
|
+
createLine(
|
|
725
|
+
index,
|
|
726
|
+
{ x: location.x + radius, y: location.y - radius },
|
|
727
|
+
{ x: location.x - radius, y: location.y + radius },
|
|
728
|
+
color,
|
|
729
|
+
0.1,
|
|
730
|
+
scale,
|
|
731
|
+
"b"
|
|
732
|
+
)
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
if (kind === "17") {
|
|
736
|
+
return convertPowerPort(record, index, context, options, color);
|
|
737
|
+
}
|
|
738
|
+
if (kind === "18") {
|
|
739
|
+
return convertPort(record, index, context, options, color);
|
|
740
|
+
}
|
|
741
|
+
if (kind === "4" || kind === "25" || kind === "34" || kind === "41") {
|
|
742
|
+
if (options.includeText === false) return [];
|
|
743
|
+
if (record.getBoolean("ISHIDDEN") && !options.includeHidden) return [];
|
|
744
|
+
const text = record.getDecoded("TEXT") ?? record.getDecoded("NAME") ?? record.getDecoded("DESIGNATOR");
|
|
745
|
+
const location = getLocation(record);
|
|
746
|
+
if (!text || !location) return [];
|
|
747
|
+
return [createText(record, index, text, location, color, context)];
|
|
748
|
+
}
|
|
749
|
+
if (kind === "28") {
|
|
750
|
+
const rectangle = getRectangle(record);
|
|
751
|
+
const text = decodeMultilineText(record.getDecoded("TEXT") ?? "");
|
|
752
|
+
if (!rectangle || !text || options.includeText === false) return [];
|
|
753
|
+
const rect = {
|
|
754
|
+
type: "schematic_rect",
|
|
755
|
+
schematic_rect_id: `schematic_text_frame_altium_${index}`,
|
|
756
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
757
|
+
center: scalePoint(
|
|
758
|
+
{
|
|
759
|
+
x: (rectangle.minX + rectangle.maxX) / 2,
|
|
760
|
+
y: (rectangle.minY + rectangle.maxY) / 2
|
|
761
|
+
},
|
|
762
|
+
scale
|
|
763
|
+
),
|
|
764
|
+
width: (rectangle.maxX - rectangle.minX) * scale,
|
|
765
|
+
height: (rectangle.maxY - rectangle.minY) * scale,
|
|
766
|
+
rotation: 0,
|
|
767
|
+
stroke_width: strokeWidth,
|
|
768
|
+
color,
|
|
769
|
+
is_filled: false,
|
|
770
|
+
is_dashed: false
|
|
771
|
+
};
|
|
772
|
+
const textElement = createText(
|
|
773
|
+
record,
|
|
774
|
+
index,
|
|
775
|
+
text,
|
|
776
|
+
{ x: rectangle.minX + 2, y: rectangle.maxY - 2 },
|
|
777
|
+
color,
|
|
778
|
+
context,
|
|
779
|
+
"top_left"
|
|
780
|
+
);
|
|
781
|
+
return [rect, textElement];
|
|
782
|
+
}
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
function convertPin(record, index, context, options, color) {
|
|
786
|
+
const location = getLocation(record);
|
|
787
|
+
if (!location) return [];
|
|
788
|
+
const pinConglomerate = record.getNumber("PINCONGLOMERATE");
|
|
789
|
+
const hidden = record.getBoolean("ISHIDDEN") || pinConglomerate !== void 0 && (pinConglomerate & 4) !== 0;
|
|
790
|
+
if (hidden && !options.includeHidden) return [];
|
|
791
|
+
const orientation = (pinConglomerate ?? Number(record.getCaseInsensitive("ORIENTATION") ?? 0)) & 3;
|
|
792
|
+
const direction = [
|
|
793
|
+
{ x: 1, y: 0 },
|
|
794
|
+
{ x: 0, y: 1 },
|
|
795
|
+
{ x: -1, y: 0 },
|
|
796
|
+
{ x: 0, y: -1 }
|
|
797
|
+
][orientation] ?? { x: 1, y: 0 };
|
|
798
|
+
const length = Math.max(
|
|
799
|
+
Number(record.getCaseInsensitive("PINLENGTH") ?? 10),
|
|
800
|
+
1
|
|
801
|
+
);
|
|
802
|
+
const end = {
|
|
803
|
+
x: location.x + direction.x * length,
|
|
804
|
+
y: location.y + direction.y * length
|
|
805
|
+
};
|
|
806
|
+
const elements = [
|
|
807
|
+
createLine(index, location, end, color, 0.1, context.scale, "pin")
|
|
808
|
+
];
|
|
809
|
+
if (options.includeText === false) return elements;
|
|
810
|
+
const name = record.getDecoded("NAME") ?? "";
|
|
811
|
+
const designator = record.getDecoded("DESIGNATOR") ?? "";
|
|
812
|
+
const showName = pinConglomerate === void 0 || (pinConglomerate & 8) !== 0;
|
|
813
|
+
const showDesignator = pinConglomerate === void 0 || (pinConglomerate & 16) !== 0;
|
|
814
|
+
const rotation = orientation === 1 || orientation === 3 ? 90 : 0;
|
|
815
|
+
const directionMatchesText = orientation === 0 || orientation === 1;
|
|
816
|
+
const nameAnchor = directionMatchesText ? "right" : "left";
|
|
817
|
+
const designatorAnchor = directionMatchesText ? "left" : "right";
|
|
818
|
+
const textOffset = 2;
|
|
819
|
+
if (showName && name) {
|
|
820
|
+
elements.push(
|
|
821
|
+
createDirectText(
|
|
822
|
+
`schematic_pin_name_altium_${index}`,
|
|
823
|
+
name,
|
|
824
|
+
{
|
|
825
|
+
x: location.x - direction.x * textOffset,
|
|
826
|
+
y: location.y - direction.y * textOffset
|
|
827
|
+
},
|
|
828
|
+
0.6,
|
|
829
|
+
color,
|
|
830
|
+
context.scale,
|
|
831
|
+
rotation,
|
|
832
|
+
nameAnchor
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (showDesignator && designator) {
|
|
837
|
+
elements.push(
|
|
838
|
+
createDirectText(
|
|
839
|
+
`schematic_pin_designator_altium_${index}`,
|
|
840
|
+
designator,
|
|
841
|
+
{
|
|
842
|
+
x: location.x + direction.x * textOffset,
|
|
843
|
+
y: location.y + direction.y * textOffset
|
|
844
|
+
},
|
|
845
|
+
0.6,
|
|
846
|
+
color,
|
|
847
|
+
context.scale,
|
|
848
|
+
rotation,
|
|
849
|
+
designatorAnchor
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return elements;
|
|
854
|
+
}
|
|
855
|
+
function convertPowerPort(record, index, context, options, color) {
|
|
856
|
+
const location = getLocation(record);
|
|
857
|
+
if (!location) return [];
|
|
858
|
+
const points = [
|
|
859
|
+
{ x: location.x, y: location.y },
|
|
860
|
+
{ x: location.x - 5, y: location.y + 7 },
|
|
861
|
+
{ x: location.x + 5, y: location.y + 7 }
|
|
862
|
+
];
|
|
863
|
+
const elements = [
|
|
864
|
+
{
|
|
865
|
+
type: "schematic_path",
|
|
866
|
+
schematic_path_id: `schematic_power_port_altium_${index}`,
|
|
867
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
868
|
+
points: points.map((point) => scalePoint(point, context.scale)),
|
|
869
|
+
stroke_width: 0.1,
|
|
870
|
+
stroke_color: color,
|
|
871
|
+
fill_color: color,
|
|
872
|
+
is_filled: true,
|
|
873
|
+
is_dashed: false
|
|
874
|
+
}
|
|
875
|
+
];
|
|
876
|
+
const text = record.getDecoded("TEXT") ?? record.getDecoded("NAME");
|
|
877
|
+
if (text && options.includeText !== false) {
|
|
878
|
+
elements.push(
|
|
879
|
+
createDirectText(
|
|
880
|
+
`schematic_power_port_text_altium_${index}`,
|
|
881
|
+
text,
|
|
882
|
+
{ x: location.x + 7, y: location.y + 3 },
|
|
883
|
+
getFontSize(record, context),
|
|
884
|
+
color,
|
|
885
|
+
context.scale,
|
|
886
|
+
0,
|
|
887
|
+
"left"
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return elements;
|
|
892
|
+
}
|
|
893
|
+
function convertPort(record, index, context, options, color) {
|
|
894
|
+
const location = getLocation(record);
|
|
895
|
+
if (!location) return [];
|
|
896
|
+
const width = Math.max(Number(record.getCaseInsensitive("WIDTH") ?? 16), 10);
|
|
897
|
+
const points = [
|
|
898
|
+
{ x: location.x, y: location.y },
|
|
899
|
+
{ x: location.x + width * 0.22, y: location.y + 5 },
|
|
900
|
+
{ x: location.x + width, y: location.y + 5 },
|
|
901
|
+
{ x: location.x + width, y: location.y - 5 },
|
|
902
|
+
{ x: location.x + width * 0.22, y: location.y - 5 }
|
|
903
|
+
];
|
|
904
|
+
const elements = [
|
|
905
|
+
{
|
|
906
|
+
type: "schematic_path",
|
|
907
|
+
schematic_path_id: `schematic_port_altium_${index}`,
|
|
908
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
909
|
+
points: points.map((point) => scalePoint(point, context.scale)),
|
|
910
|
+
stroke_width: 0.1,
|
|
911
|
+
stroke_color: color,
|
|
912
|
+
fill_color: "#ffffff",
|
|
913
|
+
is_filled: true,
|
|
914
|
+
is_dashed: false
|
|
915
|
+
}
|
|
916
|
+
];
|
|
917
|
+
const name = record.getDecoded("NAME");
|
|
918
|
+
if (name && options.includeText !== false) {
|
|
919
|
+
elements.push(
|
|
920
|
+
createDirectText(
|
|
921
|
+
`schematic_port_text_altium_${index}`,
|
|
922
|
+
name,
|
|
923
|
+
{ x: location.x + width / 2, y: location.y },
|
|
924
|
+
getFontSize(record, context),
|
|
925
|
+
color,
|
|
926
|
+
context.scale,
|
|
927
|
+
0,
|
|
928
|
+
"center"
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return elements;
|
|
933
|
+
}
|
|
934
|
+
function createSheetBorder(sheetRecord, scale) {
|
|
935
|
+
const width = Math.max(
|
|
936
|
+
Number(sheetRecord?.getCaseInsensitive("CUSTOMX") ?? 1e3),
|
|
937
|
+
1
|
|
938
|
+
);
|
|
939
|
+
const height = Math.max(
|
|
940
|
+
Number(sheetRecord?.getCaseInsensitive("CUSTOMY") ?? 800),
|
|
941
|
+
1
|
|
942
|
+
);
|
|
943
|
+
return {
|
|
944
|
+
type: "schematic_rect",
|
|
945
|
+
schematic_rect_id: "schematic_rect_altium_sheet_border",
|
|
946
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
947
|
+
center: { x: width * scale / 2, y: height * scale / 2 },
|
|
948
|
+
width: width * scale,
|
|
949
|
+
height: height * scale,
|
|
950
|
+
rotation: 0,
|
|
951
|
+
stroke_width: 0.1,
|
|
952
|
+
color: "#334155",
|
|
953
|
+
is_filled: false,
|
|
954
|
+
is_dashed: false
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function createLine(index, start, end, color, strokeWidth, scale, suffix = "line") {
|
|
958
|
+
return {
|
|
959
|
+
type: "schematic_line",
|
|
960
|
+
schematic_line_id: `schematic_line_altium_${index}_${suffix}`,
|
|
961
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
962
|
+
x1: start.x * scale,
|
|
963
|
+
y1: start.y * scale,
|
|
964
|
+
x2: end.x * scale,
|
|
965
|
+
y2: end.y * scale,
|
|
966
|
+
stroke_width: strokeWidth,
|
|
967
|
+
color,
|
|
968
|
+
is_dashed: false
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function createText(record, index, text, location, color, context, anchorOverride) {
|
|
972
|
+
const positioning = getTextPositioning(record);
|
|
973
|
+
return createDirectText(
|
|
974
|
+
`schematic_text_altium_${index}`,
|
|
975
|
+
text,
|
|
976
|
+
location,
|
|
977
|
+
getFontSize(record, context),
|
|
978
|
+
color,
|
|
979
|
+
context.scale,
|
|
980
|
+
positioning.rotation,
|
|
981
|
+
anchorOverride ?? positioning.anchor
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
function createDirectText(id, text, location, fontSize, color, scale, rotation, anchor) {
|
|
985
|
+
return {
|
|
986
|
+
type: "schematic_text",
|
|
987
|
+
schematic_text_id: id,
|
|
988
|
+
schematic_sheet_id: SCHEMATIC_SHEET_ID,
|
|
989
|
+
text,
|
|
990
|
+
font_size: Math.max(fontSize * scale, 0.2),
|
|
991
|
+
position: scalePoint(location, scale),
|
|
992
|
+
rotation,
|
|
993
|
+
anchor,
|
|
994
|
+
color
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function getFontSize(record, context) {
|
|
998
|
+
const fontId = Math.max(
|
|
999
|
+
Math.round(Number(record.getCaseInsensitive("FONTID") ?? 1)),
|
|
1000
|
+
1
|
|
1001
|
+
);
|
|
1002
|
+
return Math.max(
|
|
1003
|
+
Number(context.sheetRecord?.getCaseInsensitive(`SIZE${fontId}`) ?? 9),
|
|
1004
|
+
1
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
function getTextPositioning(record) {
|
|
1008
|
+
const justification = Math.min(
|
|
1009
|
+
Math.max(Math.round(record.getNumber("JUSTIFICATION") ?? 0), 0),
|
|
1010
|
+
8
|
|
1011
|
+
);
|
|
1012
|
+
const orientation = (Math.round(record.getNumber("ORIENTATION") ?? 0) % 4 + 4) % 4;
|
|
1013
|
+
let column = justification % 3;
|
|
1014
|
+
const row = Math.floor(justification / 3);
|
|
1015
|
+
if (orientation === 2 || orientation === 3) column = 2 - column;
|
|
1016
|
+
const horizontal = ["left", "center", "right"][column] ?? "left";
|
|
1017
|
+
const vertical = ["bottom", "center", "top"][row] ?? "bottom";
|
|
1018
|
+
const anchor = horizontal === "center" && vertical === "center" ? "center" : `${vertical}_${horizontal}`;
|
|
1019
|
+
return {
|
|
1020
|
+
anchor,
|
|
1021
|
+
rotation: orientation === 1 || orientation === 3 ? 90 : 0
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
function shouldRenderSchematicRecord(record, context) {
|
|
1025
|
+
let ownerPartId = record.getNumber("OWNERPARTID");
|
|
1026
|
+
let ownerPartDisplayMode = record.getNumber("OWNERPARTDISPLAYMODE");
|
|
1027
|
+
let current = record;
|
|
1028
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1029
|
+
while (current && !visited.has(current)) {
|
|
1030
|
+
visited.add(current);
|
|
1031
|
+
const parent = context.document.getParent(current);
|
|
1032
|
+
if (!parent) return true;
|
|
1033
|
+
if (ownerPartId === void 0 || ownerPartId <= 0) {
|
|
1034
|
+
ownerPartId = current.getNumber("OWNERPARTID");
|
|
1035
|
+
}
|
|
1036
|
+
if (ownerPartDisplayMode === void 0) {
|
|
1037
|
+
ownerPartDisplayMode = current.getNumber("OWNERPARTDISPLAYMODE");
|
|
1038
|
+
}
|
|
1039
|
+
if (parent.recordKind === "1") {
|
|
1040
|
+
const currentPartId = parent.getNumber("CURRENTPARTID") ?? 1;
|
|
1041
|
+
return (ownerPartId === void 0 || ownerPartId <= 0 || ownerPartId === currentPartId) && (ownerPartDisplayMode === void 0 || ownerPartDisplayMode === 0);
|
|
1042
|
+
}
|
|
1043
|
+
current = parent;
|
|
1044
|
+
}
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
function getLocation(record) {
|
|
1048
|
+
if (record.getCaseInsensitive("LOCATION.X") === void 0 || record.getCaseInsensitive("LOCATION.Y") === void 0) {
|
|
1049
|
+
return void 0;
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
x: getCoordinate(record, "LOCATION.X"),
|
|
1053
|
+
y: getCoordinate(record, "LOCATION.Y")
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function getCorner(record) {
|
|
1057
|
+
if (record.getCaseInsensitive("CORNER.X") === void 0 || record.getCaseInsensitive("CORNER.Y") === void 0) {
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
x: getCoordinate(record, "CORNER.X"),
|
|
1062
|
+
y: getCoordinate(record, "CORNER.Y")
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function getRectangle(record) {
|
|
1066
|
+
const location = getLocation(record);
|
|
1067
|
+
const corner = getCorner(record);
|
|
1068
|
+
if (!location || !corner) return void 0;
|
|
1069
|
+
return {
|
|
1070
|
+
minX: Math.min(location.x, corner.x),
|
|
1071
|
+
minY: Math.min(location.y, corner.y),
|
|
1072
|
+
maxX: Math.max(location.x, corner.x),
|
|
1073
|
+
maxY: Math.max(location.y, corner.y)
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function getCoordinate(record, key, fallback = 0) {
|
|
1077
|
+
const integerPart = Number(record.getCaseInsensitive(key) ?? fallback);
|
|
1078
|
+
const fractionRaw = record.getCaseInsensitive(`${key}_FRAC`);
|
|
1079
|
+
if (!Number.isFinite(integerPart) || fractionRaw === void 0) {
|
|
1080
|
+
return Number.isFinite(integerPart) ? integerPart : fallback;
|
|
1081
|
+
}
|
|
1082
|
+
const fraction = Number(`0.${fractionRaw.replace(/^[+-]/u, "")}`);
|
|
1083
|
+
if (!Number.isFinite(fraction)) return integerPart;
|
|
1084
|
+
return integerPart < 0 ? integerPart - fraction : integerPart + fraction;
|
|
1085
|
+
}
|
|
1086
|
+
function scalePoint(point, scale) {
|
|
1087
|
+
return { x: point.x * scale, y: point.y * scale };
|
|
1088
|
+
}
|
|
1089
|
+
function approximateEllipse(center, radiusX, radiusY) {
|
|
1090
|
+
return Array.from({ length: 49 }, (_, index) => {
|
|
1091
|
+
const radians = index / 48 * Math.PI * 2;
|
|
1092
|
+
return {
|
|
1093
|
+
x: center.x + Math.cos(radians) * radiusX,
|
|
1094
|
+
y: center.y + Math.sin(radians) * radiusY
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
function decodeMultilineText(text) {
|
|
1099
|
+
return text.replaceAll("~1", "\n").replaceAll("\\n", "\n");
|
|
1100
|
+
}
|
|
1101
|
+
function altiumColorToCss(raw, fallback) {
|
|
1102
|
+
if (raw === void 0) return fallback;
|
|
1103
|
+
const value = Number(raw);
|
|
1104
|
+
if (!Number.isInteger(value) || value < 0) return fallback;
|
|
1105
|
+
const red = value & 255;
|
|
1106
|
+
const green = value >>> 8 & 255;
|
|
1107
|
+
const blue = value >>> 16 & 255;
|
|
1108
|
+
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
|
|
1109
|
+
}
|
|
1110
|
+
function toHex(value) {
|
|
1111
|
+
return value.toString(16).padStart(2, "0");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// lib/convert-altium-to-circuit-json.ts
|
|
1115
|
+
import {
|
|
1116
|
+
AltiumBinaryPcbDoc,
|
|
1117
|
+
AltiumPcbDoc,
|
|
1118
|
+
AltiumSchDoc,
|
|
1119
|
+
parseAltiumFile,
|
|
1120
|
+
parseAltiumPcbDoc,
|
|
1121
|
+
parseAltiumSchDoc
|
|
1122
|
+
} from "altiumts";
|
|
1123
|
+
function convertAltiumToCircuitJson(source, options = {}) {
|
|
1124
|
+
const sourceType = options.sourceType ?? "auto";
|
|
1125
|
+
if (sourceType === "pcb") {
|
|
1126
|
+
if (typeof source === "string") {
|
|
1127
|
+
return convertAltiumPcbDocToCircuitJson(
|
|
1128
|
+
parseAltiumPcbDoc(source),
|
|
1129
|
+
options.pcb
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const { document: document2 } = parseAltiumFile(toUint8Array(source));
|
|
1133
|
+
if (!(document2 instanceof AltiumPcbDoc) && !(document2 instanceof AltiumBinaryPcbDoc)) {
|
|
1134
|
+
throw new TypeError(
|
|
1135
|
+
`Expected an Altium PCB document, got ${document2.type}`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
return convertAltiumPcbDocToCircuitJson(document2, options.pcb);
|
|
1139
|
+
}
|
|
1140
|
+
if (sourceType === "schematic") {
|
|
1141
|
+
return convertAltiumSchDocToCircuitJson(
|
|
1142
|
+
parseAltiumSchDoc(normalizeSchematicSource(source)),
|
|
1143
|
+
options.schematic
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const bytes = toUint8Array(source);
|
|
1147
|
+
const { document } = parseAltiumFile(bytes);
|
|
1148
|
+
return convertAltiumDocumentToCircuitJson(document, options);
|
|
1149
|
+
}
|
|
1150
|
+
function convertAltiumDocumentToCircuitJson(document, options = {}) {
|
|
1151
|
+
if (document instanceof AltiumSchDoc) {
|
|
1152
|
+
return convertAltiumSchDocToCircuitJson(document, options.schematic);
|
|
1153
|
+
}
|
|
1154
|
+
if (document instanceof AltiumPcbDoc || document instanceof AltiumBinaryPcbDoc) {
|
|
1155
|
+
return convertAltiumPcbDocToCircuitJson(document, options.pcb);
|
|
1156
|
+
}
|
|
1157
|
+
const type = typeof document === "object" && document !== null && "type" in document ? String(document.type) : typeof document;
|
|
1158
|
+
throw new TypeError(`Unsupported Altium document type: ${type}`);
|
|
1159
|
+
}
|
|
1160
|
+
function toUint8Array(source) {
|
|
1161
|
+
if (typeof source === "string") return new TextEncoder().encode(source);
|
|
1162
|
+
if (source instanceof Uint8Array) return source;
|
|
1163
|
+
return new Uint8Array(source);
|
|
1164
|
+
}
|
|
1165
|
+
function normalizeSchematicSource(source) {
|
|
1166
|
+
return source instanceof ArrayBuffer ? new Uint8Array(source) : source;
|
|
1167
|
+
}
|
|
1168
|
+
export {
|
|
1169
|
+
convertAltiumDocumentToCircuitJson,
|
|
1170
|
+
convertAltiumPcbDocToCircuitJson,
|
|
1171
|
+
convertAltiumSchDocToCircuitJson,
|
|
1172
|
+
convertAltiumToCircuitJson
|
|
1173
|
+
};
|
|
1174
|
+
//# sourceMappingURL=index.js.map
|