altium-toolkit 1.1.32 → 1.1.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.32",
3
+ "version": "1.1.33",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -0,0 +1,551 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds generated schematic and PCB primitive records for compact libraries.
7
+ */
8
+ export class AltiumGeneratedLibraryRecordBuilder {
9
+ /**
10
+ * Builds a full schematic component record stream.
11
+ * @param {object} bundle Normalized component bundle.
12
+ * @returns {string}
13
+ */
14
+ static buildSchematicComponentData(bundle) {
15
+ return [
16
+ AltiumGeneratedLibraryRecordBuilder.#pipeRecord({
17
+ RECORD: 'Component',
18
+ Name: bundle.symbol.name,
19
+ SourceId: bundle.id,
20
+ DisplayName: bundle.name,
21
+ PinCount: String(bundle.symbol.pins.length),
22
+ PrimitiveCount: String(bundle.symbol.primitives.length)
23
+ }),
24
+ ...bundle.symbol.pins.map((pin) =>
25
+ AltiumGeneratedLibraryRecordBuilder.#schematicPinRecord(pin)
26
+ ),
27
+ ...bundle.symbol.primitives.map((primitive) =>
28
+ AltiumGeneratedLibraryRecordBuilder.#schematicPrimitiveRecord(
29
+ primitive
30
+ )
31
+ )
32
+ ].join('')
33
+ }
34
+
35
+ /**
36
+ * Counts generated footprint primitives.
37
+ * @param {object} footprint Normalized footprint.
38
+ * @returns {number}
39
+ */
40
+ static footprintPrimitiveCount(footprint) {
41
+ return (
42
+ AltiumGeneratedLibraryRecordBuilder.#array(footprint.pads).length +
43
+ AltiumGeneratedLibraryRecordBuilder.#array(footprint.tracks)
44
+ .length +
45
+ AltiumGeneratedLibraryRecordBuilder.#array(footprint.arcs).length +
46
+ AltiumGeneratedLibraryRecordBuilder.#array(footprint.fills).length +
47
+ AltiumGeneratedLibraryRecordBuilder.#array(footprint.texts).length
48
+ )
49
+ }
50
+
51
+ /**
52
+ * Builds binary PcbLib footprint data after the leading name block.
53
+ * @param {object} footprint Normalized footprint.
54
+ * @returns {Uint8Array[]}
55
+ */
56
+ static buildFootprintPrimitiveRecords(footprint) {
57
+ return [
58
+ ...AltiumGeneratedLibraryRecordBuilder.#array(footprint.pads).map(
59
+ (pad) => AltiumGeneratedLibraryRecordBuilder.#padRecord(pad)
60
+ ),
61
+ ...AltiumGeneratedLibraryRecordBuilder.#array(footprint.tracks).map(
62
+ (track) =>
63
+ AltiumGeneratedLibraryRecordBuilder.#trackRecord(track)
64
+ ),
65
+ ...AltiumGeneratedLibraryRecordBuilder.#array(footprint.arcs).map(
66
+ (arc) => AltiumGeneratedLibraryRecordBuilder.#arcRecord(arc)
67
+ ),
68
+ ...AltiumGeneratedLibraryRecordBuilder.#array(footprint.fills).map(
69
+ (fill) => AltiumGeneratedLibraryRecordBuilder.#fillRecord(fill)
70
+ ),
71
+ ...AltiumGeneratedLibraryRecordBuilder.#array(footprint.texts).map(
72
+ (text) => AltiumGeneratedLibraryRecordBuilder.#textRecord(text)
73
+ )
74
+ ]
75
+ }
76
+
77
+ /**
78
+ * Builds one schematic pin record.
79
+ * @param {object} pin Normalized pin.
80
+ * @returns {string}
81
+ */
82
+ static #schematicPinRecord(pin) {
83
+ return AltiumGeneratedLibraryRecordBuilder.#pipeRecord({
84
+ RECORD: 'Pin',
85
+ Designator:
86
+ pin.number || pin.pinNumber || pin.designator || pin.name,
87
+ Name: pin.name || pin.pinName || pin.designator,
88
+ ElectricalType:
89
+ AltiumGeneratedLibraryRecordBuilder.#schematicElectricalType(
90
+ pin
91
+ ),
92
+ 'Location.X': AltiumGeneratedLibraryRecordBuilder.#numberText(
93
+ pin.x,
94
+ 0
95
+ ),
96
+ 'Location.Y': AltiumGeneratedLibraryRecordBuilder.#numberText(
97
+ pin.y,
98
+ 0
99
+ ),
100
+ PinLength: AltiumGeneratedLibraryRecordBuilder.#numberText(
101
+ pin.length,
102
+ 10
103
+ )
104
+ })
105
+ }
106
+
107
+ /**
108
+ * Resolves one schematic pin electrical type label.
109
+ * @param {object} pin Normalized pin.
110
+ * @returns {string}
111
+ */
112
+ static #schematicElectricalType(pin) {
113
+ if (typeof pin.electrical === 'string') return pin.electrical
114
+
115
+ return (
116
+ {
117
+ 0: 'input',
118
+ 1: 'io',
119
+ 2: 'output',
120
+ 3: 'open-collector',
121
+ 4: 'passive',
122
+ 5: 'hi-z',
123
+ 6: 'open-emitter',
124
+ 7: 'power'
125
+ }[Number(pin.electrical)] || 'passive'
126
+ )
127
+ }
128
+
129
+ /**
130
+ * Builds one schematic primitive record.
131
+ * @param {object} primitive Normalized primitive.
132
+ * @returns {string}
133
+ */
134
+ static #schematicPrimitiveRecord(primitive) {
135
+ const type =
136
+ AltiumGeneratedLibraryRecordBuilder.#primitiveType(primitive)
137
+
138
+ return AltiumGeneratedLibraryRecordBuilder.#pipeRecord({
139
+ RECORD: type,
140
+ 'Location.X': AltiumGeneratedLibraryRecordBuilder.#numberText(
141
+ primitive.x ?? primitive.x1,
142
+ 0
143
+ ),
144
+ 'Location.Y': AltiumGeneratedLibraryRecordBuilder.#numberText(
145
+ primitive.y ?? primitive.y1,
146
+ 0
147
+ ),
148
+ 'Corner.X': AltiumGeneratedLibraryRecordBuilder.#numberText(
149
+ primitive.cornerX ??
150
+ primitive.x2 ??
151
+ Number(primitive.x || 0) + Number(primitive.width || 0),
152
+ 0
153
+ ),
154
+ 'Corner.Y': AltiumGeneratedLibraryRecordBuilder.#numberText(
155
+ primitive.cornerY ??
156
+ primitive.y2 ??
157
+ Number(primitive.y || 0) + Number(primitive.height || 0),
158
+ 0
159
+ )
160
+ })
161
+ }
162
+
163
+ /**
164
+ * Resolves a schematic primitive record type.
165
+ * @param {object} primitive Normalized primitive.
166
+ * @returns {string}
167
+ */
168
+ static #primitiveType(primitive) {
169
+ const type = String(primitive.type || primitive.recordType || '')
170
+ .trim()
171
+ .toLowerCase()
172
+ if (type === 'line' || ('x1' in primitive && 'x2' in primitive)) {
173
+ return 'Line'
174
+ }
175
+ if (type === 'arc') return 'Arc'
176
+ if (type === 'ellipse') return 'Ellipse'
177
+ if (type === 'polygon') return 'Polygon'
178
+
179
+ return 'Rectangle'
180
+ }
181
+
182
+ /**
183
+ * Builds one pad primitive record.
184
+ * @param {object} pad Normalized pad.
185
+ * @returns {Uint8Array}
186
+ */
187
+ static #padRecord(pad) {
188
+ const main = new Uint8Array(118)
189
+ const view = new DataView(main.buffer)
190
+ const width = AltiumGeneratedLibraryRecordBuilder.#number(
191
+ pad.width ?? pad.sizeTopX ?? pad.sizeMidX ?? pad.sizeBottomX,
192
+ 1
193
+ )
194
+ const height = AltiumGeneratedLibraryRecordBuilder.#number(
195
+ pad.height ?? pad.sizeTopY ?? pad.sizeMidY ?? pad.sizeBottomY,
196
+ 1
197
+ )
198
+ const layerId = AltiumGeneratedLibraryRecordBuilder.#layerId(pad, 1)
199
+ const shape = AltiumGeneratedLibraryRecordBuilder.#padShape(pad)
200
+
201
+ view.setUint8(0, layerId)
202
+ view.setUint16(1, 8, true)
203
+ AltiumGeneratedLibraryRecordBuilder.#writeOwnerIndexes(view, pad, {
204
+ net: 3,
205
+ polygon: 5,
206
+ component: 7
207
+ })
208
+ view.setUint32(9, 0xffffffff, true)
209
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 13, pad.x, 0)
210
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 17, pad.y, 0)
211
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 21, width, 1)
212
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 25, height, 1)
213
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 29, width, 1)
214
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 33, height, 1)
215
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 37, width, 1)
216
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 41, height, 1)
217
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(
218
+ view,
219
+ 45,
220
+ pad.holeDiameter,
221
+ 0
222
+ )
223
+ view.setUint8(49, shape)
224
+ view.setUint8(50, shape)
225
+ view.setUint8(51, shape)
226
+ view.setFloat64(
227
+ 52,
228
+ AltiumGeneratedLibraryRecordBuilder.#number(pad.rotation, 0),
229
+ true
230
+ )
231
+ view.setUint8(60, pad.isPlated === false ? 0 : 1)
232
+ view.setUint8(62, 0)
233
+ view.setUint8(101, 1)
234
+ view.setUint8(102, 1)
235
+
236
+ return AltiumGeneratedLibraryRecordBuilder.#concatBytes([
237
+ new Uint8Array([2]),
238
+ ...[0, 1, 2, 3].map(() =>
239
+ AltiumGeneratedLibraryRecordBuilder.#lengthPrefixed(
240
+ new Uint8Array()
241
+ )
242
+ ),
243
+ AltiumGeneratedLibraryRecordBuilder.#lengthPrefixed(main),
244
+ AltiumGeneratedLibraryRecordBuilder.#lengthPrefixed(
245
+ new Uint8Array()
246
+ )
247
+ ])
248
+ }
249
+
250
+ /**
251
+ * Builds one track primitive record.
252
+ * @param {object} track Normalized track.
253
+ * @returns {Uint8Array}
254
+ */
255
+ static #trackRecord(track) {
256
+ const payload = new Uint8Array(49)
257
+ const view = new DataView(payload.buffer)
258
+
259
+ view.setUint8(0, AltiumGeneratedLibraryRecordBuilder.#layerId(track, 1))
260
+ AltiumGeneratedLibraryRecordBuilder.#writeOwnerIndexes(view, track, {
261
+ net: 3,
262
+ polygon: 5,
263
+ component: 7
264
+ })
265
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 13, track.x1, 0)
266
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 17, track.y1, 0)
267
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 21, track.x2, 0)
268
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 25, track.y2, 0)
269
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 29, track.width, 1)
270
+
271
+ return AltiumGeneratedLibraryRecordBuilder.#objectRecord(4, payload)
272
+ }
273
+
274
+ /**
275
+ * Builds one arc primitive record.
276
+ * @param {object} arc Normalized arc.
277
+ * @returns {Uint8Array}
278
+ */
279
+ static #arcRecord(arc) {
280
+ const payload = new Uint8Array(45)
281
+ const view = new DataView(payload.buffer)
282
+
283
+ view.setUint8(0, AltiumGeneratedLibraryRecordBuilder.#layerId(arc, 1))
284
+ AltiumGeneratedLibraryRecordBuilder.#writeOwnerIndexes(view, arc, {
285
+ net: 3,
286
+ polygon: 5,
287
+ component: 7
288
+ })
289
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 13, arc.x, 0)
290
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 17, arc.y, 0)
291
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 21, arc.radius, 1)
292
+ view.setFloat64(
293
+ 25,
294
+ AltiumGeneratedLibraryRecordBuilder.#number(arc.startAngle, 0),
295
+ true
296
+ )
297
+ view.setFloat64(
298
+ 33,
299
+ AltiumGeneratedLibraryRecordBuilder.#number(arc.endAngle, 360),
300
+ true
301
+ )
302
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 41, arc.width, 1)
303
+
304
+ return AltiumGeneratedLibraryRecordBuilder.#objectRecord(1, payload)
305
+ }
306
+
307
+ /**
308
+ * Builds one fill primitive record.
309
+ * @param {object} fill Normalized fill.
310
+ * @returns {Uint8Array}
311
+ */
312
+ static #fillRecord(fill) {
313
+ const bytes = new Uint8Array(55)
314
+ const view = new DataView(bytes.buffer)
315
+ const layerId = AltiumGeneratedLibraryRecordBuilder.#layerId(fill, 1)
316
+
317
+ bytes[0] = 6
318
+ view.setUint32(1, 50, true)
319
+ view.setUint8(5, layerId)
320
+ AltiumGeneratedLibraryRecordBuilder.#writeOwnerIndexes(view, fill, {
321
+ net: 8,
322
+ polygon: 10,
323
+ component: 12
324
+ })
325
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 18, fill.x1, 0)
326
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 22, fill.y1, 0)
327
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 26, fill.x2, 0)
328
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 30, fill.y2, 0)
329
+ view.setUint16(46, layerId, true)
330
+
331
+ return bytes
332
+ }
333
+
334
+ /**
335
+ * Builds one PCB text primitive record.
336
+ * @param {object} text Normalized text.
337
+ * @returns {Uint8Array}
338
+ */
339
+ static #textRecord(text) {
340
+ const payload = new Uint8Array(64)
341
+ const view = new DataView(payload.buffer)
342
+ const textBytes = new TextEncoder().encode(String(text.text || ''))
343
+
344
+ view.setUint8(0, AltiumGeneratedLibraryRecordBuilder.#layerId(text, 21))
345
+ view.setInt16(
346
+ 7,
347
+ AltiumGeneratedLibraryRecordBuilder.#nullableIndex(
348
+ text.ownerIndex ?? text.componentIndex
349
+ ),
350
+ true
351
+ )
352
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 13, text.x, 0)
353
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 17, text.y, 0)
354
+ AltiumGeneratedLibraryRecordBuilder.#writeMil(view, 21, text.height, 10)
355
+ view.setUint32(25, 0, true)
356
+ view.setUint32(41, 0, true)
357
+
358
+ return AltiumGeneratedLibraryRecordBuilder.#concatBytes([
359
+ AltiumGeneratedLibraryRecordBuilder.#objectRecord(5, payload),
360
+ AltiumGeneratedLibraryRecordBuilder.#lengthPrefixed(textBytes)
361
+ ])
362
+ }
363
+
364
+ /**
365
+ * Creates one object-id and payload-length prefixed primitive record.
366
+ * @param {number} objectId Primitive object id.
367
+ * @param {Uint8Array} payload Payload bytes.
368
+ * @returns {Uint8Array}
369
+ */
370
+ static #objectRecord(objectId, payload) {
371
+ const bytes = new Uint8Array(5 + payload.byteLength)
372
+ const view = new DataView(bytes.buffer)
373
+
374
+ bytes[0] = Number(objectId)
375
+ view.setUint32(1, payload.byteLength, true)
376
+ bytes.set(payload, 5)
377
+
378
+ return bytes
379
+ }
380
+
381
+ /**
382
+ * Creates one four-byte length-prefixed byte block.
383
+ * @param {Uint8Array} payload Payload bytes.
384
+ * @returns {Uint8Array}
385
+ */
386
+ static #lengthPrefixed(payload) {
387
+ const bytes = new Uint8Array(4 + payload.byteLength)
388
+ const view = new DataView(bytes.buffer)
389
+
390
+ view.setUint32(0, payload.byteLength, true)
391
+ bytes.set(payload, 4)
392
+
393
+ return bytes
394
+ }
395
+
396
+ /**
397
+ * Writes owner indexes at the supplied offsets.
398
+ * @param {DataView} view Target view.
399
+ * @param {object} primitive Source primitive.
400
+ * @param {{ component: number, net: number, polygon: number }} offsets Field offsets.
401
+ * @returns {void}
402
+ */
403
+ static #writeOwnerIndexes(view, primitive, offsets) {
404
+ view.setUint16(
405
+ offsets.component,
406
+ AltiumGeneratedLibraryRecordBuilder.#nullableIndex(
407
+ primitive.componentIndex ?? primitive.ownerIndex
408
+ ),
409
+ true
410
+ )
411
+ view.setUint16(
412
+ offsets.net,
413
+ AltiumGeneratedLibraryRecordBuilder.#nullableIndex(
414
+ primitive.netIndex
415
+ ),
416
+ true
417
+ )
418
+ view.setUint16(
419
+ offsets.polygon,
420
+ AltiumGeneratedLibraryRecordBuilder.#nullableIndex(
421
+ primitive.polygonIndex
422
+ ),
423
+ true
424
+ )
425
+ }
426
+
427
+ /**
428
+ * Converts one optional index into Altium's nullable index encoding.
429
+ * @param {unknown} value Source value.
430
+ * @returns {number}
431
+ */
432
+ static #nullableIndex(value) {
433
+ const number = Number(value)
434
+ return Number.isInteger(number) && number >= 0 ? number : 0xffff
435
+ }
436
+
437
+ /**
438
+ * Writes one mil value as Altium fixed-point integer.
439
+ * @param {DataView} view Target view.
440
+ * @param {number} offset Byte offset.
441
+ * @param {unknown} value Source value.
442
+ * @param {number} fallback Fallback value.
443
+ * @returns {void}
444
+ */
445
+ static #writeMil(view, offset, value, fallback) {
446
+ view.setInt32(
447
+ offset,
448
+ Math.round(
449
+ AltiumGeneratedLibraryRecordBuilder.#number(value, fallback) *
450
+ 10000
451
+ ),
452
+ true
453
+ )
454
+ }
455
+
456
+ /**
457
+ * Resolves one layer id.
458
+ * @param {object} primitive Source primitive.
459
+ * @param {number} fallback Fallback layer id.
460
+ * @returns {number}
461
+ */
462
+ static #layerId(primitive, fallback) {
463
+ return AltiumGeneratedLibraryRecordBuilder.#number(
464
+ primitive.layerId ?? primitive.layerCode ?? primitive.legacyLayerId,
465
+ fallback
466
+ )
467
+ }
468
+
469
+ /**
470
+ * Resolves one pad shape id.
471
+ * @param {object} pad Source pad.
472
+ * @returns {number}
473
+ */
474
+ static #padShape(pad) {
475
+ const shape = String(
476
+ pad.shapeTopName || pad.shape || pad.padShape || ''
477
+ ).toLowerCase()
478
+ if (shape.includes('round')) return 1
479
+ if (shape.includes('oct')) return 3
480
+
481
+ return Number(pad.shapeTop || pad.shapeMid || pad.shapeBottom || 2)
482
+ }
483
+
484
+ /**
485
+ * Returns a finite number or fallback.
486
+ * @param {unknown} value Source value.
487
+ * @param {number} fallback Fallback value.
488
+ * @returns {number}
489
+ */
490
+ static #number(value, fallback) {
491
+ const number = Number(value)
492
+ return Number.isFinite(number) ? number : fallback
493
+ }
494
+
495
+ /**
496
+ * Returns one numeric field as a compact string.
497
+ * @param {unknown} value Source value.
498
+ * @param {number} fallback Fallback value.
499
+ * @returns {string}
500
+ */
501
+ static #numberText(value, fallback) {
502
+ return String(
503
+ AltiumGeneratedLibraryRecordBuilder.#number(value, fallback)
504
+ )
505
+ }
506
+
507
+ /**
508
+ * Builds one pipe-delimited record.
509
+ * @param {Record<string, string>} fields Record fields.
510
+ * @returns {string}
511
+ */
512
+ static #pipeRecord(fields) {
513
+ return (
514
+ '|' +
515
+ Object.entries(fields)
516
+ .filter(([, value]) => String(value ?? '') !== '')
517
+ .map(([key, value]) => key + '=' + String(value))
518
+ .join('|')
519
+ )
520
+ }
521
+
522
+ /**
523
+ * Concatenates byte chunks.
524
+ * @param {Uint8Array[]} chunks Byte chunks.
525
+ * @returns {Uint8Array}
526
+ */
527
+ static #concatBytes(chunks) {
528
+ const byteLength = chunks.reduce(
529
+ (sum, chunk) => sum + chunk.byteLength,
530
+ 0
531
+ )
532
+ const bytes = new Uint8Array(byteLength)
533
+ let offset = 0
534
+
535
+ for (const chunk of chunks) {
536
+ bytes.set(chunk, offset)
537
+ offset += chunk.byteLength
538
+ }
539
+
540
+ return bytes
541
+ }
542
+
543
+ /**
544
+ * Normalizes an unknown value to an array.
545
+ * @param {unknown} value Source value.
546
+ * @returns {Array}
547
+ */
548
+ static #array(value) {
549
+ return Array.isArray(value) ? value : []
550
+ }
551
+ }
@@ -2,6 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ import { AltiumGeneratedLibraryRecordBuilder } from './AltiumGeneratedLibraryRecordBuilder.mjs'
6
+
5
7
  /**
6
8
  * Builds deterministic textual and byte records for generated Altium libraries.
7
9
  */
@@ -22,6 +24,17 @@ export class AltiumLibraryRecordBuilder {
22
24
  })
23
25
  }
24
26
 
27
+ /**
28
+ * Builds a full schematic component data record stream.
29
+ * @param {object} bundle Normalized component bundle.
30
+ * @returns {string}
31
+ */
32
+ static buildSchematicComponentDataRecord(bundle) {
33
+ return AltiumGeneratedLibraryRecordBuilder.buildSchematicComponentData(
34
+ bundle
35
+ )
36
+ }
37
+
25
38
  /**
26
39
  * Builds a PCB footprint record.
27
40
  * @param {object} bundle Normalized component bundle.
@@ -35,10 +48,24 @@ export class AltiumLibraryRecordBuilder {
35
48
  DisplayName: bundle.name,
36
49
  PadCount: String(bundle.footprint.pads.length),
37
50
  TrackCount: String(bundle.footprint.tracks.length),
51
+ ArcCount: String(bundle.footprint.arcs.length),
52
+ FillCount: String(bundle.footprint.fills.length),
53
+ TextCount: String(bundle.footprint.texts.length),
38
54
  ModelCount: String(bundle.models.length)
39
55
  })
40
56
  }
41
57
 
58
+ /**
59
+ * Counts generated footprint primitives.
60
+ * @param {object} bundle Normalized bundle.
61
+ * @returns {number}
62
+ */
63
+ static footprintPrimitiveCount(bundle) {
64
+ return AltiumGeneratedLibraryRecordBuilder.footprintPrimitiveCount(
65
+ bundle.footprint
66
+ )
67
+ }
68
+
42
69
  /**
43
70
  * Builds a PcbLib Library/Data stream.
44
71
  * @param {object[]} bundles Normalized component bundles.
@@ -113,7 +140,10 @@ export class AltiumLibraryRecordBuilder {
113
140
  */
114
141
  static buildFootprintData(bundle) {
115
142
  return AltiumLibraryRecordBuilder.concatBytes([
116
- AltiumLibraryRecordBuilder.createStringBlock(bundle.footprint.name)
143
+ AltiumLibraryRecordBuilder.createStringBlock(bundle.footprint.name),
144
+ ...AltiumGeneratedLibraryRecordBuilder.buildFootprintPrimitiveRecords(
145
+ bundle.footprint
146
+ )
117
147
  ])
118
148
  }
119
149
 
@@ -42,7 +42,9 @@ export class AltiumPcbLibExporter {
42
42
  )
43
43
  streams.set(
44
44
  storageName + '/Header',
45
- AltiumLibraryRecordBuilder.createCountHeader(0)
45
+ AltiumLibraryRecordBuilder.createCountHeader(
46
+ AltiumLibraryRecordBuilder.footprintPrimitiveCount(bundle)
47
+ )
46
48
  )
47
49
  streams.set(
48
50
  storageName + '/Parameters',
@@ -34,7 +34,7 @@ export class AltiumSchLibExporter {
34
34
  ) +
35
35
  '/Data',
36
36
  new TextEncoder().encode(
37
- AltiumLibraryRecordBuilder.buildSchematicComponentRecord(
37
+ AltiumLibraryRecordBuilder.buildSchematicComponentDataRecord(
38
38
  bundle
39
39
  )
40
40
  )
@@ -498,7 +498,14 @@ export class PcbEmbeddedModelExtractor {
498
498
  componentBody.dzMil
499
499
  ].join('\u0000')
500
500
 
501
- if (!uniqueBodies.has(key)) {
501
+ const existingBody = uniqueBodies.get(key)
502
+ if (
503
+ !existingBody ||
504
+ PcbEmbeddedModelExtractor.#shouldPreferComponentBody(
505
+ componentBody,
506
+ existingBody
507
+ )
508
+ ) {
502
509
  uniqueBodies.set(key, componentBody)
503
510
  }
504
511
  }
@@ -506,6 +513,36 @@ export class PcbEmbeddedModelExtractor {
506
513
  return [...uniqueBodies.values()]
507
514
  }
508
515
 
516
+ /**
517
+ * Checks whether one component-body row carries better static geometry.
518
+ * @param {{ staticGeometry?: object }} candidate Candidate body row.
519
+ * @param {{ staticGeometry?: object }} existing Existing body row.
520
+ * @returns {boolean}
521
+ */
522
+ static #shouldPreferComponentBody(candidate, existing) {
523
+ return (
524
+ PcbEmbeddedModelExtractor.#staticGeometryRank(candidate) >
525
+ PcbEmbeddedModelExtractor.#staticGeometryRank(existing)
526
+ )
527
+ }
528
+
529
+ /**
530
+ * Ranks static geometry completeness for duplicate body rows.
531
+ * @param {{ staticGeometry?: object }} componentBody Component body row.
532
+ * @returns {number}
533
+ */
534
+ static #staticGeometryRank(componentBody) {
535
+ if (componentBody?.staticGeometry?.status === 'complete') {
536
+ return 2
537
+ }
538
+
539
+ if (componentBody?.staticGeometry) {
540
+ return 1
541
+ }
542
+
543
+ return 0
544
+ }
545
+
509
546
  /**
510
547
  * Builds model metadata and payload integrity diagnostics.
511
548
  * @param {object[]} metadataRows Parsed model metadata rows.