altium-toolkit 1.1.24 → 1.1.25

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.
@@ -7,6 +7,7 @@
7
7
  // specifier.
8
8
  import { unzlibSync } from 'fflate'
9
9
  import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
10
+ import { PcbShapeBasedBodyGeometryParser } from './PcbShapeBasedBodyGeometryParser.mjs'
10
11
 
11
12
  /**
12
13
  * Extracts embedded 3D model payloads and component-body placement metadata
@@ -17,7 +18,7 @@ export class PcbEmbeddedModelExtractor {
17
18
  * Extracts embedded model payloads and component-body placements from one
18
19
  * stream map.
19
20
  * @param {Map<string, Uint8Array>} streams
20
- * @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[] }}
21
+ * @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[] }}
21
22
  */
22
23
  static extractFromStreams(streams) {
23
24
  const modelStreamPrefix =
@@ -51,7 +52,7 @@ export class PcbEmbeddedModelExtractor {
51
52
  streams.get('ComponentBodies6/Data'),
52
53
  'ComponentBodies6/Data'
53
54
  ),
54
- ...PcbEmbeddedModelExtractor.#parseComponentBodyStream(
55
+ ...PcbEmbeddedModelExtractor.#parseShapeBasedComponentBodyStream(
55
56
  streams.get('ShapeBasedComponentBodies6/Data'),
56
57
  'ShapeBasedComponentBodies6/Data'
57
58
  )
@@ -134,9 +135,10 @@ export class PcbEmbeddedModelExtractor {
134
135
  * Parses one component-body printable stream into model-placement records.
135
136
  * @param {Uint8Array | undefined} bytes
136
137
  * @param {string} sourceStream
137
- * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[]}
138
+ * @param {{ inferStaticGeometry?: boolean }} [options]
139
+ * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
138
140
  */
139
- static #parseComponentBodyStream(bytes, sourceStream) {
141
+ static #parseComponentBodyStream(bytes, sourceStream, options = {}) {
140
142
  if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
141
143
  return []
142
144
  }
@@ -153,12 +155,53 @@ export class PcbEmbeddedModelExtractor {
153
155
  .map((fields) =>
154
156
  PcbEmbeddedModelExtractor.#normalizeComponentBody(
155
157
  fields,
156
- sourceStream
158
+ sourceStream,
159
+ options.inferStaticGeometry
160
+ ? {
161
+ staticGeometry:
162
+ PcbShapeBasedBodyGeometryParser.buildStaticGeometry(
163
+ fields,
164
+ []
165
+ )
166
+ }
167
+ : {}
157
168
  )
158
169
  )
159
170
  .filter(Boolean)
160
171
  }
161
172
 
173
+ /**
174
+ * Parses shape-based component bodies, including optional vertex payloads.
175
+ * @param {Uint8Array | undefined} bytes
176
+ * @param {string} sourceStream
177
+ * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
178
+ */
179
+ static #parseShapeBasedComponentBodyStream(bytes, sourceStream) {
180
+ if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
181
+ return []
182
+ }
183
+
184
+ const records = PcbShapeBasedBodyGeometryParser.parse(bytes)
185
+
186
+ if (records.length) {
187
+ return records
188
+ .map((record) =>
189
+ PcbEmbeddedModelExtractor.#normalizeComponentBody(
190
+ record.fields,
191
+ sourceStream,
192
+ { staticGeometry: record.staticGeometry }
193
+ )
194
+ )
195
+ .filter(Boolean)
196
+ }
197
+
198
+ return PcbEmbeddedModelExtractor.#parseComponentBodyStream(
199
+ bytes,
200
+ sourceStream,
201
+ { inferStaticGeometry: true }
202
+ )
203
+ }
204
+
162
205
  /**
163
206
  * Parses one printable field record without requiring a specific leading
164
207
  * marker such as `|RECORD=` or `|KIND=`.
@@ -266,9 +309,10 @@ export class PcbEmbeddedModelExtractor {
266
309
  * Normalizes one component-body record into model-placement metadata.
267
310
  * @param {Record<string, string | string[]>} fields
268
311
  * @param {string} sourceStream
269
- * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null } | null}
312
+ * @param {{ staticGeometry?: object }} [extra]
313
+ * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object } | null}
270
314
  */
271
- static #normalizeComponentBody(fields, sourceStream) {
315
+ static #normalizeComponentBody(fields, sourceStream, extra = {}) {
272
316
  const modelId = PcbEmbeddedModelExtractor.#getField(fields, 'MODELID')
273
317
  const name = PcbEmbeddedModelExtractor.#getField(fields, 'MODEL.NAME')
274
318
 
@@ -339,7 +383,82 @@ export class PcbEmbeddedModelExtractor {
339
383
  standoffHeightMil: PcbEmbeddedModelExtractor.#parseMilLikeField(
340
384
  fields,
341
385
  'STANDOFFHEIGHT'
342
- )
386
+ ),
387
+ ...PcbEmbeddedModelExtractor.#shapeBodyMetadata(fields),
388
+ ...PcbEmbeddedModelExtractor.#stripUndefined(extra)
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Normalizes shape-based body display metadata.
394
+ * @param {Record<string, string | string[]>} fields Native fields.
395
+ * @returns {object}
396
+ */
397
+ static #shapeBodyMetadata(fields) {
398
+ const modelType = PcbEmbeddedModelExtractor.#parseIntegerField(
399
+ fields,
400
+ 'MODEL.MODELTYPE'
401
+ )
402
+ const bodyColorRaw = PcbEmbeddedModelExtractor.#parseIntegerField(
403
+ fields,
404
+ 'BODYCOLOR3D'
405
+ )
406
+ const bodyOpacity = PcbEmbeddedModelExtractor.#parseNumberField(
407
+ fields,
408
+ 'BODYOPACITY3D'
409
+ )
410
+
411
+ return PcbEmbeddedModelExtractor.#stripUndefined({
412
+ modelType: modelType === null ? undefined : modelType,
413
+ modelTypeName:
414
+ modelType === null
415
+ ? undefined
416
+ : PcbEmbeddedModelExtractor.#modelTypeName(modelType),
417
+ bodyColor:
418
+ bodyColorRaw === null
419
+ ? undefined
420
+ : PcbEmbeddedModelExtractor.#decodeBodyColor(bodyColorRaw),
421
+ bodyOpacity:
422
+ bodyOpacity === null || !Number.isFinite(bodyOpacity)
423
+ ? undefined
424
+ : bodyOpacity
425
+ })
426
+ }
427
+
428
+ /**
429
+ * Maps a numeric body model type to a stable semantic label.
430
+ * @param {number} modelType Numeric model type.
431
+ * @returns {string}
432
+ */
433
+ static #modelTypeName(modelType) {
434
+ const modelTypeNames = new Map([
435
+ [0, 'extruded-polygon'],
436
+ [1, 'cone'],
437
+ [2, 'cylinder'],
438
+ [3, 'sphere']
439
+ ])
440
+
441
+ return modelTypeNames.get(modelType) || 'unknown-' + modelType
442
+ }
443
+
444
+ /**
445
+ * Decodes one packed native RGB color value.
446
+ * @param {number} rawColor Packed color value.
447
+ * @returns {{ raw: number, hex: string, rgb: { red: number, green: number, blue: number } }}
448
+ */
449
+ static #decodeBodyColor(rawColor) {
450
+ const red = rawColor & 0xff
451
+ const green = (rawColor >> 8) & 0xff
452
+ const blue = (rawColor >> 16) & 0xff
453
+
454
+ return {
455
+ raw: rawColor,
456
+ hex:
457
+ '#' +
458
+ [red, green, blue]
459
+ .map((component) => component.toString(16).padStart(2, '0'))
460
+ .join(''),
461
+ rgb: { red, green, blue }
343
462
  }
344
463
  }
345
464
 
@@ -359,8 +478,8 @@ export class PcbEmbeddedModelExtractor {
359
478
 
360
479
  /**
361
480
  * Deduplicates shape-based body records shared across body streams.
362
- * @param {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[]} componentBodies
363
- * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[]}
481
+ * @param {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]} componentBodies
482
+ * @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
364
483
  */
365
484
  static #dedupeComponentBodies(componentBodies) {
366
485
  const uniqueBodies = new Map()
@@ -747,4 +866,15 @@ export class PcbEmbeddedModelExtractor {
747
866
  .filter(Number.isInteger)
748
867
  )
749
868
  }
869
+
870
+ /**
871
+ * Removes undefined values from a normalized metadata row.
872
+ * @param {object} row Metadata row.
873
+ * @returns {object}
874
+ */
875
+ static #stripUndefined(row) {
876
+ return Object.fromEntries(
877
+ Object.entries(row).filter(([, value]) => value !== undefined)
878
+ )
879
+ }
750
880
  }