altium-toolkit 1.0.7 → 1.0.9

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.
Files changed (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -24,12 +24,22 @@ export class PcbEmbeddedModelExtractor {
24
24
  PcbEmbeddedModelExtractor.#parseModelMetadataStream(
25
25
  streams.get('Models/Data')
26
26
  )
27
- const models = modelMetadataRecords
28
- .map((record, index) =>
27
+ const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
28
+ fields,
29
+ index,
30
+ sourceStream: 'Models/' + index,
31
+ id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
32
+ name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
33
+ checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
34
+ PcbEmbeddedModelExtractor.#parseIntegerField(fields, 'CHECKSUM')
35
+ )
36
+ }))
37
+ const models = modelMetadataRows
38
+ .map((row) =>
29
39
  PcbEmbeddedModelExtractor.#normalizeEmbeddedModel(
30
- record,
31
- streams.get('Models/' + index),
32
- 'Models/' + index
40
+ row.fields,
41
+ streams.get(row.sourceStream),
42
+ row.sourceStream
33
43
  )
34
44
  )
35
45
  .filter(Boolean)
@@ -44,10 +54,18 @@ export class PcbEmbeddedModelExtractor {
44
54
  'ShapeBasedComponentBodies6/Data'
45
55
  )
46
56
  ])
57
+ const integrity = PcbEmbeddedModelExtractor.#buildIntegrityReport(
58
+ modelMetadataRows,
59
+ models,
60
+ componentBodies,
61
+ streams
62
+ )
47
63
 
48
64
  return {
49
65
  models,
50
- componentBodies
66
+ componentBodies,
67
+ integrity,
68
+ diagnostics: integrity.issues
51
69
  }
52
70
  }
53
71
 
@@ -350,6 +368,193 @@ export class PcbEmbeddedModelExtractor {
350
368
  return [...uniqueBodies.values()]
351
369
  }
352
370
 
371
+ /**
372
+ * Builds model metadata and payload integrity diagnostics.
373
+ * @param {object[]} metadataRows Parsed model metadata rows.
374
+ * @param {object[]} models Recovered payload models.
375
+ * @param {object[]} componentBodies Recovered component bodies.
376
+ * @param {Map<string, Uint8Array>} streams Compound streams.
377
+ * @returns {{ schema: string, issues: object[] }}
378
+ */
379
+ static #buildIntegrityReport(
380
+ metadataRows,
381
+ models,
382
+ componentBodies,
383
+ streams
384
+ ) {
385
+ const issues = [
386
+ ...PcbEmbeddedModelExtractor.#missingPayloadIssues(
387
+ metadataRows,
388
+ models,
389
+ streams
390
+ ),
391
+ ...PcbEmbeddedModelExtractor.#duplicateChecksumIssues(metadataRows),
392
+ ...PcbEmbeddedModelExtractor.#unresolvedBodyIssues(
393
+ componentBodies,
394
+ models
395
+ ),
396
+ ...PcbEmbeddedModelExtractor.#unreferencedModelIssues(
397
+ models,
398
+ componentBodies
399
+ )
400
+ ]
401
+
402
+ return {
403
+ schema: 'altium-toolkit.pcb.embedded-model-integrity.a1',
404
+ issues
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Reports metadata rows without a recoverable payload stream.
410
+ * @param {object[]} metadataRows Parsed metadata rows.
411
+ * @param {object[]} models Recovered model rows.
412
+ * @param {Map<string, Uint8Array>} streams Compound streams.
413
+ * @returns {object[]}
414
+ */
415
+ static #missingPayloadIssues(metadataRows, models, streams) {
416
+ return (metadataRows || [])
417
+ .filter(
418
+ (row) =>
419
+ !streams.has(row.sourceStream) ||
420
+ !models.some((model) => model.id === row.id)
421
+ )
422
+ .map((row) => ({
423
+ code: streams.has(row.sourceStream)
424
+ ? 'pcb.model.payload-unreadable'
425
+ : 'pcb.model.payload-missing',
426
+ severity: 'warning',
427
+ modelId: row.id,
428
+ checksum: row.checksum,
429
+ name: row.name,
430
+ sourceStream: row.sourceStream,
431
+ message: streams.has(row.sourceStream)
432
+ ? 'Embedded model payload stream could not be decoded.'
433
+ : 'Embedded model metadata references a missing payload stream.'
434
+ }))
435
+ }
436
+
437
+ /**
438
+ * Reports duplicate authored model checksums.
439
+ * @param {object[]} metadataRows Parsed metadata rows.
440
+ * @returns {object[]}
441
+ */
442
+ static #duplicateChecksumIssues(metadataRows) {
443
+ const rowsByChecksum = new Map()
444
+
445
+ for (const row of metadataRows || []) {
446
+ if (!Number.isInteger(row.checksum)) {
447
+ continue
448
+ }
449
+ if (!rowsByChecksum.has(row.checksum)) {
450
+ rowsByChecksum.set(row.checksum, [])
451
+ }
452
+ rowsByChecksum.get(row.checksum).push(row)
453
+ }
454
+
455
+ return [...rowsByChecksum.entries()]
456
+ .filter(([, rows]) => rows.length > 1)
457
+ .map(([checksum, rows]) => ({
458
+ code: 'pcb.model.checksum-duplicate',
459
+ severity: 'warning',
460
+ checksum,
461
+ modelIds: rows.map((row) => row.id).filter(Boolean),
462
+ sourceStreams: rows.map((row) => row.sourceStream),
463
+ message:
464
+ 'Multiple embedded model metadata rows share one checksum.'
465
+ }))
466
+ }
467
+
468
+ /**
469
+ * Reports component bodies that reference no recovered model.
470
+ * @param {object[]} componentBodies Component-body rows.
471
+ * @param {object[]} models Recovered model rows.
472
+ * @returns {object[]}
473
+ */
474
+ static #unresolvedBodyIssues(componentBodies, models) {
475
+ return (componentBodies || [])
476
+ .filter(
477
+ (componentBody) =>
478
+ componentBody.embedded &&
479
+ !PcbEmbeddedModelExtractor.#bodyMatchesAnyModel(
480
+ componentBody,
481
+ models
482
+ )
483
+ )
484
+ .map((componentBody) => ({
485
+ code: 'pcb.model.body-unresolved',
486
+ severity: 'warning',
487
+ modelId: componentBody.modelId,
488
+ checksum: componentBody.checksum,
489
+ name: componentBody.name,
490
+ sourceStream: componentBody.sourceStream,
491
+ message:
492
+ 'Component body references an embedded model that was not recovered.'
493
+ }))
494
+ }
495
+
496
+ /**
497
+ * Reports recovered model payloads not referenced by any component body.
498
+ * @param {object[]} models Recovered model rows.
499
+ * @param {object[]} componentBodies Component-body rows.
500
+ * @returns {object[]}
501
+ */
502
+ static #unreferencedModelIssues(models, componentBodies) {
503
+ if (!(componentBodies || []).length) {
504
+ return []
505
+ }
506
+
507
+ return (models || [])
508
+ .filter(
509
+ (model) =>
510
+ !(componentBodies || []).some((componentBody) =>
511
+ PcbEmbeddedModelExtractor.#bodyMatchesModel(
512
+ componentBody,
513
+ model
514
+ )
515
+ )
516
+ )
517
+ .map((model) => ({
518
+ code: 'pcb.model.payload-unreferenced',
519
+ severity: 'info',
520
+ modelId: model.id,
521
+ checksum: model.checksum,
522
+ name: model.name,
523
+ sourceStream: model.sourceStream,
524
+ message:
525
+ 'Embedded model payload was recovered but no component body references it.'
526
+ }))
527
+ }
528
+
529
+ /**
530
+ * Returns true when a component body matches any recovered model.
531
+ * @param {object} componentBody Component body.
532
+ * @param {object[]} models Recovered models.
533
+ * @returns {boolean}
534
+ */
535
+ static #bodyMatchesAnyModel(componentBody, models) {
536
+ return (models || []).some((model) =>
537
+ PcbEmbeddedModelExtractor.#bodyMatchesModel(componentBody, model)
538
+ )
539
+ }
540
+
541
+ /**
542
+ * Returns true when a component body references a model.
543
+ * @param {object} componentBody Component body.
544
+ * @param {object} model Recovered model.
545
+ * @returns {boolean}
546
+ */
547
+ static #bodyMatchesModel(componentBody, model) {
548
+ return (
549
+ (componentBody.modelId && componentBody.modelId === model.id) ||
550
+ (Number.isInteger(componentBody.checksum) &&
551
+ componentBody.checksum === model.checksum) ||
552
+ (componentBody.name &&
553
+ String(componentBody.name).toLowerCase() ===
554
+ String(model.name || '').toLowerCase())
555
+ )
556
+ }
557
+
353
558
  /**
354
559
  * Resolves one model format from metadata and payload text.
355
560
  * @param {string} name
@@ -374,6 +579,27 @@ export class PcbEmbeddedModelExtractor {
374
579
  return 'wrl'
375
580
  }
376
581
 
582
+ if (
583
+ normalizedName.endsWith('.sldprt') ||
584
+ normalizedName.endsWith('.sldasm')
585
+ ) {
586
+ return 'solidworks'
587
+ }
588
+
589
+ if (
590
+ normalizedName.endsWith('.x_t') ||
591
+ normalizedName.endsWith('.xmt_txt')
592
+ ) {
593
+ return 'parasolid-text'
594
+ }
595
+
596
+ if (
597
+ normalizedName.endsWith('.x_b') ||
598
+ normalizedName.endsWith('.xmt_bin')
599
+ ) {
600
+ return 'parasolid-binary'
601
+ }
602
+
377
603
  return 'unknown'
378
604
  }
379
605
 
@@ -0,0 +1,256 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
6
+
7
+ /**
8
+ * Decodes extended primitive sidecar records such as mask-expansion overrides.
9
+ */
10
+ export class PcbExtendedPrimitiveInformationParser {
11
+ static #SOURCE_STREAM = 'ExtendedPrimitiveInformation/Data'
12
+
13
+ static #OBJECT_ID_TO_COLLECTION = {
14
+ 1: ['arcs', 'arc'],
15
+ 2: ['pads', 'pad'],
16
+ 3: ['vias', 'via'],
17
+ 4: ['tracks', 'track'],
18
+ 5: ['texts', 'text'],
19
+ 6: ['fills', 'fill'],
20
+ 11: ['regions', 'region']
21
+ }
22
+
23
+ /**
24
+ * Parses extended primitive information records.
25
+ * @param {Uint8Array | ArrayBuffer | undefined} dataBytes
26
+ * @param {string} [sourceStream]
27
+ * @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
28
+ */
29
+ static parse(
30
+ dataBytes,
31
+ sourceStream = PcbExtendedPrimitiveInformationParser.#SOURCE_STREAM
32
+ ) {
33
+ const entries = PcbSidecarRecordParser.parseLengthPrefixedRecords(
34
+ dataBytes,
35
+ sourceStream
36
+ )
37
+ .map((record) =>
38
+ PcbExtendedPrimitiveInformationParser.#normalizeRecord(record)
39
+ )
40
+ .filter(Boolean)
41
+
42
+ return PcbExtendedPrimitiveInformationParser.#buildLookups(entries)
43
+ }
44
+
45
+ /**
46
+ * Adds extended primitive information to matching decoded primitives.
47
+ * @param {Record<string, object[]>} binaryPrimitives
48
+ * @param {{ entries?: object[] }} extendedInformation
49
+ */
50
+ static attachToPrimitives(binaryPrimitives, extendedInformation) {
51
+ if (!binaryPrimitives || !Array.isArray(extendedInformation?.entries)) {
52
+ return
53
+ }
54
+
55
+ for (const entry of extendedInformation.entries) {
56
+ const collectionName =
57
+ PcbExtendedPrimitiveInformationParser.#collectionNameForEntry(
58
+ entry
59
+ )
60
+ const collection = binaryPrimitives[collectionName]
61
+
62
+ if (!Array.isArray(collection)) {
63
+ continue
64
+ }
65
+
66
+ const primitive = collection[entry.primitiveIndex]
67
+ if (!primitive) {
68
+ continue
69
+ }
70
+
71
+ primitive.extendedPrimitiveInformation =
72
+ PcbExtendedPrimitiveInformationParser.#publicEntry(entry)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Normalizes one decoded sidecar record.
78
+ * @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
79
+ * @returns {object | null}
80
+ */
81
+ static #normalizeRecord(record) {
82
+ const primitiveIndex = PcbSidecarRecordParser.parseInteger(
83
+ PcbSidecarRecordParser.firstField(record.fields, [
84
+ 'PRIMITIVEINDEX',
85
+ 'INDEX'
86
+ ])
87
+ )
88
+
89
+ if (primitiveIndex === null) {
90
+ return null
91
+ }
92
+
93
+ const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
94
+ PcbSidecarRecordParser.firstField(record.fields, [
95
+ 'PRIMITIVEOBJECTID',
96
+ 'OBJECTID'
97
+ ])
98
+ )
99
+ const objectInfo =
100
+ PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
101
+ primitiveObjectId
102
+ ] || []
103
+ const type = PcbSidecarRecordParser.firstField(record.fields, ['TYPE'])
104
+ const primitiveType =
105
+ objectInfo[1] ||
106
+ PcbExtendedPrimitiveInformationParser.#normalizePrimitiveType(type)
107
+
108
+ return {
109
+ primitiveIndex,
110
+ primitiveObjectId,
111
+ primitiveType,
112
+ type,
113
+ sourceStream: record.sourceStream,
114
+ maskExpansion:
115
+ PcbExtendedPrimitiveInformationParser.#parseMaskExpansion(
116
+ record.fields
117
+ ),
118
+ fields: record.fields
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Parses paste and solder mask-expansion fields.
124
+ * @param {Record<string, string>} fields
125
+ * @returns {{ paste: object, solder: object }}
126
+ */
127
+ static #parseMaskExpansion(fields) {
128
+ const pasteMode = PcbSidecarRecordParser.parseInteger(
129
+ PcbSidecarRecordParser.firstField(fields, [
130
+ 'PASTEMASKEXPANSIONMODE',
131
+ 'PASTEMASKEXPANSION_MODE'
132
+ ])
133
+ )
134
+ const solderMode = PcbSidecarRecordParser.parseInteger(
135
+ PcbSidecarRecordParser.firstField(fields, [
136
+ 'SOLDERMASKEXPANSIONMODE',
137
+ 'SOLDERMASKEXPANSION_MODE'
138
+ ])
139
+ )
140
+
141
+ return {
142
+ paste: {
143
+ mode: pasteMode,
144
+ source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
145
+ pasteMode
146
+ ),
147
+ manualExpansion: PcbSidecarRecordParser.parseNumber(
148
+ PcbSidecarRecordParser.firstField(fields, [
149
+ 'PASTEMASKEXPANSION_MANUAL',
150
+ 'PASTEMASKEXPANSIONMANUAL'
151
+ ])
152
+ )
153
+ },
154
+ solder: {
155
+ mode: solderMode,
156
+ source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
157
+ solderMode
158
+ ),
159
+ manualExpansion: PcbSidecarRecordParser.parseNumber(
160
+ PcbSidecarRecordParser.firstField(fields, [
161
+ 'SOLDERMASKEXPANSION_MANUAL',
162
+ 'SOLDERMASKEXPANSIONMANUAL'
163
+ ])
164
+ )
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Builds primitive-index lookups.
171
+ * @param {object[]} entries
172
+ * @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
173
+ */
174
+ static #buildLookups(entries) {
175
+ const byPrimitiveIndex = {}
176
+ const byPrimitiveKey = {}
177
+
178
+ for (const entry of entries) {
179
+ byPrimitiveIndex[String(entry.primitiveIndex)] = entry
180
+ if (Number.isInteger(entry.primitiveObjectId)) {
181
+ byPrimitiveKey[
182
+ entry.primitiveObjectId + ':' + entry.primitiveIndex
183
+ ] = entry
184
+ }
185
+ }
186
+
187
+ return {
188
+ entries,
189
+ byPrimitiveIndex,
190
+ byPrimitiveKey
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Resolves one binary primitive collection for a sidecar entry.
196
+ * @param {object} entry
197
+ * @returns {string}
198
+ */
199
+ static #collectionNameForEntry(entry) {
200
+ const objectInfo =
201
+ PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
202
+ entry.primitiveObjectId
203
+ ]
204
+ if (objectInfo) {
205
+ return objectInfo[0]
206
+ }
207
+
208
+ const primitiveType = String(entry.primitiveType || '').toLowerCase()
209
+ return primitiveType ? primitiveType + 's' : ''
210
+ }
211
+
212
+ /**
213
+ * Builds the public primitive-attached entry.
214
+ * @param {object} entry
215
+ * @returns {object}
216
+ */
217
+ static #publicEntry(entry) {
218
+ return {
219
+ primitiveIndex: entry.primitiveIndex,
220
+ primitiveObjectId: entry.primitiveObjectId,
221
+ primitiveType: entry.primitiveType,
222
+ type: entry.type,
223
+ sourceStream: entry.sourceStream,
224
+ maskExpansion: entry.maskExpansion
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Maps one mask-expansion mode to the existing source vocabulary.
230
+ * @param {number | null} mode
231
+ * @returns {string}
232
+ */
233
+ static #maskExpansionSource(mode) {
234
+ if (mode === 1) return 'rule'
235
+ if (mode === 2) return 'manual'
236
+ if (mode === 0) return 'default'
237
+ if (mode === null) return 'unknown'
238
+
239
+ return 'unknown-' + mode
240
+ }
241
+
242
+ /**
243
+ * Normalizes one textual primitive type hint.
244
+ * @param {string} value
245
+ * @returns {string}
246
+ */
247
+ static #normalizePrimitiveType(value) {
248
+ const normalized = String(value || '')
249
+ .trim()
250
+ .toLowerCase()
251
+ .replace(/^e/iu, '')
252
+ .replace(/object$/iu, '')
253
+
254
+ return normalized
255
+ }
256
+ }