altium-toolkit 0.1.23 → 1.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.
@@ -0,0 +1,354 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ const MILS_PER_MM = 39.37007874015748
6
+
7
+ /**
8
+ * Shared primitive conversions for Circuit JSON adapters.
9
+ */
10
+ export class CircuitJsonModelAdapterPrimitives {
11
+ /**
12
+ * Returns a deterministic id scope for one parsed file.
13
+ * @param {Record<string, unknown>} model
14
+ * @param {string} sourceFormat
15
+ * @returns {string}
16
+ */
17
+ static idScope(model, sourceFormat) {
18
+ return CircuitJsonModelAdapterPrimitives.id('', [
19
+ sourceFormat,
20
+ model.fileType || 'document',
21
+ model.fileName || model.summary?.title || 'untitled'
22
+ ])
23
+ }
24
+
25
+ /**
26
+ * Returns a deterministic Circuit JSON id.
27
+ * @param {string} scope
28
+ * @param {unknown[]} parts
29
+ * @returns {string}
30
+ */
31
+ static id(scope, parts) {
32
+ const idParts = [scope, ...parts]
33
+ .filter(
34
+ (part) => part !== undefined && part !== null && part !== ''
35
+ )
36
+ .map((part) => CircuitJsonModelAdapterPrimitives.#idPart(part))
37
+
38
+ return ['cj', ...idParts].filter(Boolean).join('_')
39
+ }
40
+
41
+ /**
42
+ * Returns a string value.
43
+ * @param {unknown} value
44
+ * @param {string} fallback
45
+ * @returns {string}
46
+ */
47
+ static string(value, fallback) {
48
+ const text = String(value ?? '').trim()
49
+ return text || fallback
50
+ }
51
+
52
+ /**
53
+ * Returns a finite number value.
54
+ * @param {unknown} value
55
+ * @param {number | null} fallback
56
+ * @returns {number | null}
57
+ */
58
+ static number(value, fallback) {
59
+ const numeric = Number(value)
60
+ return Number.isFinite(numeric) ? numeric : fallback
61
+ }
62
+
63
+ /**
64
+ * Converts a mil value to millimeters.
65
+ * @param {unknown} value
66
+ * @param {number} fallback
67
+ * @returns {number}
68
+ */
69
+ static milNumber(value, fallback) {
70
+ return CircuitJsonModelAdapterPrimitives.round(
71
+ (CircuitJsonModelAdapterPrimitives.number(value, fallback) || 0) /
72
+ MILS_PER_MM
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Returns an unscaled point.
78
+ * @param {unknown} x
79
+ * @param {unknown} y
80
+ * @returns {{ x: number, y: number }}
81
+ */
82
+ static point(x, y) {
83
+ return {
84
+ x: CircuitJsonModelAdapterPrimitives.round(
85
+ CircuitJsonModelAdapterPrimitives.number(x, 0) || 0
86
+ ),
87
+ y: CircuitJsonModelAdapterPrimitives.round(
88
+ CircuitJsonModelAdapterPrimitives.number(y, 0) || 0
89
+ )
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Returns a mil-scaled point.
95
+ * @param {unknown} x
96
+ * @param {unknown} y
97
+ * @returns {{ x: number, y: number }}
98
+ */
99
+ static milPoint(x, y) {
100
+ return {
101
+ x: CircuitJsonModelAdapterPrimitives.milNumber(x, 0),
102
+ y: CircuitJsonModelAdapterPrimitives.milNumber(y, 0)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Rounds model coordinates to a stable precision.
108
+ * @param {number} value
109
+ * @returns {number}
110
+ */
111
+ static round(value) {
112
+ return Math.round(value * 1_000_000) / 1_000_000
113
+ }
114
+
115
+ /**
116
+ * Returns array values only.
117
+ * @param {unknown} value
118
+ * @returns {unknown[]}
119
+ */
120
+ static array(value) {
121
+ return Array.isArray(value) ? value : []
122
+ }
123
+
124
+ /**
125
+ * Infers the source format label.
126
+ * @param {Record<string, unknown>} model
127
+ * @returns {string}
128
+ */
129
+ static sourceFormat(model) {
130
+ if (model.sourceFormat) return String(model.sourceFormat)
131
+ if (
132
+ String(model.fileType || '')
133
+ .toLowerCase()
134
+ .includes('kicad')
135
+ ) {
136
+ return 'KiCad'
137
+ }
138
+ return 'Altium Designer'
139
+ }
140
+
141
+ /**
142
+ * Returns a component map key.
143
+ * @param {Record<string, unknown>} component
144
+ * @param {number} componentIndex
145
+ * @returns {string}
146
+ */
147
+ static componentKey(component, componentIndex) {
148
+ return String(component.componentIndex ?? componentIndex)
149
+ }
150
+
151
+ /**
152
+ * Returns a source port id.
153
+ * @param {string} idScope
154
+ * @param {Record<string, unknown>} primitive
155
+ * @param {number} index
156
+ * @param {string} sourceComponentId
157
+ * @returns {string}
158
+ */
159
+ static sourcePortId(idScope, primitive, index, sourceComponentId) {
160
+ return CircuitJsonModelAdapterPrimitives.id(idScope, [
161
+ 'source_port',
162
+ sourceComponentId,
163
+ primitive.name || primitive.pinName || primitive.pinNumber || index
164
+ ])
165
+ }
166
+
167
+ /**
168
+ * Returns a source net id.
169
+ * @param {string} idScope
170
+ * @param {unknown} netName
171
+ * @returns {string}
172
+ */
173
+ static sourceNetId(idScope, netName) {
174
+ return CircuitJsonModelAdapterPrimitives.id(idScope, [
175
+ 'source_net',
176
+ netName
177
+ ])
178
+ }
179
+
180
+ /**
181
+ * Returns or creates a source net id for a PCB primitive.
182
+ * @param {string} idScope
183
+ * @param {Record<string, unknown>} primitive
184
+ * @param {Map<string, string>} sourceNetIds
185
+ * @returns {string | undefined}
186
+ */
187
+ static netIdForPrimitive(idScope, primitive, sourceNetIds) {
188
+ const key = String(
189
+ primitive.netName || primitive.net || primitive.netIndex || ''
190
+ )
191
+ if (!key) return undefined
192
+ if (!sourceNetIds.has(key)) {
193
+ sourceNetIds.set(
194
+ key,
195
+ CircuitJsonModelAdapterPrimitives.sourceNetId(idScope, key)
196
+ )
197
+ }
198
+ return sourceNetIds.get(key)
199
+ }
200
+
201
+ /**
202
+ * Returns true when a PCB pad has a drill hole.
203
+ * @param {Record<string, unknown>} pad
204
+ * @returns {boolean}
205
+ */
206
+ static isThroughHolePad(pad) {
207
+ return (
208
+ (CircuitJsonModelAdapterPrimitives.number(pad.holeDiameter, 0) ||
209
+ 0) > 0
210
+ )
211
+ }
212
+
213
+ /**
214
+ * Returns a Circuit JSON pad shape label.
215
+ * @param {Record<string, unknown>} pad
216
+ * @returns {string}
217
+ */
218
+ static padShape(pad) {
219
+ const shape = String(
220
+ pad.shapeTopName || pad.shapeName || pad.shape || ''
221
+ ).toLowerCase()
222
+ if (shape.includes('round') || shape.includes('circle')) return 'circle'
223
+ if (shape.includes('oval')) return 'pill'
224
+ return 'rect'
225
+ }
226
+
227
+ /**
228
+ * Returns a normalized board side.
229
+ * @param {unknown} layer
230
+ * @returns {'top' | 'bottom'}
231
+ */
232
+ static side(layer) {
233
+ return String(layer || '')
234
+ .toLowerCase()
235
+ .includes('bottom')
236
+ ? 'bottom'
237
+ : 'top'
238
+ }
239
+
240
+ /**
241
+ * Returns a normalized copper layer name.
242
+ * @param {Record<string, unknown>} primitive
243
+ * @returns {string}
244
+ */
245
+ static layerName(primitive) {
246
+ if (primitive.layerName) return String(primitive.layerName)
247
+ if (primitive.layer) return String(primitive.layer).toLowerCase()
248
+ if (primitive.layerId === 1) return 'top'
249
+ if (primitive.layerId === 32) return 'bottom'
250
+ return 'top'
251
+ }
252
+
253
+ /**
254
+ * Returns schematic port facing direction.
255
+ * @param {Record<string, unknown>} pin
256
+ * @returns {string | null}
257
+ */
258
+ static facingDirection(pin) {
259
+ const orientation = String(pin.orientation || '').toLowerCase()
260
+ if (['left', 'right', 'up', 'down'].includes(orientation)) {
261
+ return orientation
262
+ }
263
+ if (orientation === 'top') return 'up'
264
+ if (orientation === 'bottom') return 'down'
265
+ return null
266
+ }
267
+
268
+ /**
269
+ * Returns true when a schematic text represents a net label.
270
+ * @param {Record<string, unknown>} text
271
+ * @returns {boolean}
272
+ */
273
+ static isNetLabel(text) {
274
+ const role = String(text.role || text.kind || text.recordType || '')
275
+ .toLowerCase()
276
+ .trim()
277
+ return (
278
+ role.includes('net') ||
279
+ role.includes('label') ||
280
+ role.includes('power')
281
+ )
282
+ }
283
+
284
+ /**
285
+ * Converts a renderer outline to Circuit JSON points.
286
+ * @param {Record<string, unknown> | undefined} boardOutline
287
+ * @returns {{ x: number, y: number }[]}
288
+ */
289
+ static outlinePoints(boardOutline) {
290
+ const segments = CircuitJsonModelAdapterPrimitives.array(
291
+ boardOutline?.segments
292
+ )
293
+ if (segments.length > 0) {
294
+ const points = segments.map((segment) =>
295
+ CircuitJsonModelAdapterPrimitives.milPoint(
296
+ segment.x1,
297
+ segment.y1
298
+ )
299
+ )
300
+ const last = segments[segments.length - 1]
301
+ points.push(
302
+ CircuitJsonModelAdapterPrimitives.milPoint(last.x2, last.y2)
303
+ )
304
+ return points
305
+ }
306
+
307
+ const minX =
308
+ CircuitJsonModelAdapterPrimitives.number(boardOutline?.minX, 0) || 0
309
+ const minY =
310
+ CircuitJsonModelAdapterPrimitives.number(boardOutline?.minY, 0) || 0
311
+ const width =
312
+ CircuitJsonModelAdapterPrimitives.number(
313
+ boardOutline?.widthMil,
314
+ 0
315
+ ) || 0
316
+ const height =
317
+ CircuitJsonModelAdapterPrimitives.number(
318
+ boardOutline?.heightMil,
319
+ 0
320
+ ) || 0
321
+
322
+ return [
323
+ CircuitJsonModelAdapterPrimitives.milPoint(minX, minY),
324
+ CircuitJsonModelAdapterPrimitives.milPoint(minX + width, minY),
325
+ CircuitJsonModelAdapterPrimitives.milPoint(
326
+ minX + width,
327
+ minY + height
328
+ ),
329
+ CircuitJsonModelAdapterPrimitives.milPoint(minX, minY + height)
330
+ ]
331
+ }
332
+
333
+ /**
334
+ * Strips the extension from a file name.
335
+ * @param {unknown} fileName
336
+ * @returns {string}
337
+ */
338
+ static stripExtension(fileName) {
339
+ return String(fileName || '').replace(/\.[^.]+$/u, '')
340
+ }
341
+
342
+ /**
343
+ * Normalizes one id part.
344
+ * @param {unknown} value
345
+ * @returns {string}
346
+ */
347
+ static #idPart(value) {
348
+ return String(value)
349
+ .trim()
350
+ .toLowerCase()
351
+ .replace(/[^a-z0-9]+/g, '_')
352
+ .replace(/^_+|_+$/g, '')
353
+ }
354
+ }
@@ -0,0 +1,83 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Defines the Circuit JSON contract emitted by public parser roots.
7
+ */
8
+ export class CircuitJsonModelSchema {
9
+ static CURRENT_SCHEMA_ID = 'https://github.com/tscircuit/circuit-json'
10
+
11
+ static CURRENT_SCHEMA_VERSION = '0.0.431'
12
+
13
+ static FORMAT_NAME = 'circuit-json'
14
+
15
+ /**
16
+ * Marks a Circuit JSON array with non-serialized schema metadata.
17
+ * @template {object[]} T
18
+ * @param {T} circuitJson
19
+ * @returns {T}
20
+ */
21
+ static attach(circuitJson) {
22
+ CircuitJsonModelSchema.assertModel(circuitJson)
23
+ Object.defineProperties(circuitJson, {
24
+ circuitJsonSchema: {
25
+ configurable: true,
26
+ enumerable: false,
27
+ value: CircuitJsonModelSchema.CURRENT_SCHEMA_ID,
28
+ writable: true
29
+ },
30
+ circuitJsonVersion: {
31
+ configurable: true,
32
+ enumerable: false,
33
+ value: CircuitJsonModelSchema.CURRENT_SCHEMA_VERSION,
34
+ writable: true
35
+ },
36
+ circuitJsonFormat: {
37
+ configurable: true,
38
+ enumerable: false,
39
+ value: CircuitJsonModelSchema.FORMAT_NAME,
40
+ writable: true
41
+ }
42
+ })
43
+
44
+ return circuitJson
45
+ }
46
+
47
+ /**
48
+ * Returns true when the value is a Circuit JSON element.
49
+ * @param {unknown} value
50
+ * @returns {boolean}
51
+ */
52
+ static isElement(value) {
53
+ return (
54
+ !!value &&
55
+ typeof value === 'object' &&
56
+ typeof value.type === 'string' &&
57
+ value.type.length > 0
58
+ )
59
+ }
60
+
61
+ /**
62
+ * Returns true when the value is a Circuit JSON model array.
63
+ * @param {unknown} value
64
+ * @returns {boolean}
65
+ */
66
+ static isModel(value) {
67
+ return (
68
+ Array.isArray(value) &&
69
+ value.every((element) => CircuitJsonModelSchema.isElement(element))
70
+ )
71
+ }
72
+
73
+ /**
74
+ * Throws when a value is not a Circuit JSON model array.
75
+ * @param {unknown} value
76
+ * @returns {void}
77
+ */
78
+ static assertModel(value) {
79
+ if (!CircuitJsonModelSchema.isModel(value)) {
80
+ throw new TypeError('Expected a Circuit JSON element array.')
81
+ }
82
+ }
83
+ }