altium-toolkit 1.1.3 → 1.1.22

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 (39) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  8. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  9. package/src/core/altium/AltiumParser.mjs +5 -2
  10. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  11. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  12. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  13. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  14. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  15. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  16. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  17. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  18. package/src/core/altium/SchematicPinParser.mjs +87 -20
  19. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  20. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  21. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  22. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  23. package/src/core/altium/SourceComponentClient.mjs +239 -0
  24. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  25. package/src/parser.mjs +8 -0
  26. package/src/styles/altium-renderers.css +6 -6
  27. package/src/ui/PcbArcUtils.mjs +19 -2
  28. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  29. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  30. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  31. package/src/ui/SchematicColorResolver.mjs +185 -0
  32. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  33. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  34. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  35. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  36. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  37. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  38. package/src/ui/SchematicShapeRenderer.mjs +37 -11
  39. package/src/ui/SchematicSvgRenderer.mjs +944 -51
@@ -0,0 +1,101 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
6
+ import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
7
+ import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
8
+
9
+ /**
10
+ * Exports normalized bundles into a compact OLE-backed footprint library.
11
+ */
12
+ export class AltiumPcbLibExporter {
13
+ /**
14
+ * Exports one or more component bundles as `.PcbLib` bytes.
15
+ * @param {object[] | object} bundles Component bundles.
16
+ * @returns {Uint8Array}
17
+ */
18
+ static export(bundles) {
19
+ const normalizedBundles =
20
+ AltiumPcbLibExporter.#normalizeBundles(bundles)
21
+ const streams = new Map()
22
+ const modelRows = AltiumPcbLibExporter.#collectModels(normalizedBundles)
23
+
24
+ streams.set(
25
+ 'Library/Data',
26
+ AltiumLibraryRecordBuilder.buildPcbLibraryData(normalizedBundles)
27
+ )
28
+ streams.set(
29
+ 'Library/ComponentParamsTOC/Data',
30
+ AltiumLibraryRecordBuilder.buildComponentParamsToc(
31
+ normalizedBundles
32
+ )
33
+ )
34
+ streams.set(
35
+ 'SectionKeys',
36
+ AltiumLibraryRecordBuilder.buildSectionKeys(normalizedBundles)
37
+ )
38
+
39
+ for (const bundle of normalizedBundles) {
40
+ const storageName = AltiumLibraryRecordBuilder.sanitizeStorageName(
41
+ bundle.footprint.name
42
+ )
43
+ streams.set(
44
+ storageName + '/Header',
45
+ AltiumLibraryRecordBuilder.createCountHeader(0)
46
+ )
47
+ streams.set(
48
+ storageName + '/Parameters',
49
+ AltiumLibraryRecordBuilder.buildFootprintParameters(bundle)
50
+ )
51
+ streams.set(
52
+ storageName + '/Data',
53
+ AltiumLibraryRecordBuilder.buildFootprintData(bundle)
54
+ )
55
+ streams.set(
56
+ storageName + '/SourceRecord',
57
+ new TextEncoder().encode(
58
+ AltiumLibraryRecordBuilder.buildPcbFootprintRecord(bundle)
59
+ )
60
+ )
61
+ }
62
+
63
+ if (modelRows.length) {
64
+ streams.set(
65
+ 'Models/Data',
66
+ AltiumLibraryRecordBuilder.buildModelsData(modelRows)
67
+ )
68
+ modelRows.forEach((row, index) => {
69
+ streams.set('Models/' + index, row.model.bytes)
70
+ })
71
+ }
72
+
73
+ return OleCompoundDocumentWriter.write({ streams })
74
+ }
75
+
76
+ /**
77
+ * Collects model rows with deterministic generated ids.
78
+ * @param {object[]} bundles Normalized bundles.
79
+ * @returns {{ model: object, id: string, checksum: number }[]}
80
+ */
81
+ static #collectModels(bundles) {
82
+ return bundles.flatMap((bundle, bundleIndex) =>
83
+ bundle.models.map((model, modelIndex) => ({
84
+ model,
85
+ id: 'model-' + bundleIndex + '-' + modelIndex,
86
+ checksum: AltiumLibraryRecordBuilder.checksumBytes(model.bytes)
87
+ }))
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Normalizes one or more bundles.
93
+ * @param {object[] | object} bundles Bundle input.
94
+ * @returns {object[]}
95
+ */
96
+ static #normalizeBundles(bundles) {
97
+ return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
98
+ SourceComponentBundleNormalizer.normalize(bundle)
99
+ )
100
+ }
101
+ }
@@ -0,0 +1,57 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
6
+ import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
7
+ import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
8
+
9
+ /**
10
+ * Exports normalized bundles into a compact OLE-backed schematic library.
11
+ */
12
+ export class AltiumSchLibExporter {
13
+ /**
14
+ * Exports one or more component bundles as `.SchLib` bytes.
15
+ * @param {object[] | object} bundles Component bundles.
16
+ * @returns {Uint8Array}
17
+ */
18
+ static export(bundles) {
19
+ const normalizedBundles =
20
+ AltiumSchLibExporter.#normalizeBundles(bundles)
21
+ const streams = new Map()
22
+ const libraryRecord = normalizedBundles
23
+ .map((bundle) =>
24
+ AltiumLibraryRecordBuilder.buildSchematicComponentRecord(bundle)
25
+ )
26
+ .join('\n')
27
+
28
+ streams.set('Library/Data', new TextEncoder().encode(libraryRecord))
29
+ for (const bundle of normalizedBundles) {
30
+ streams.set(
31
+ 'Components/' +
32
+ AltiumLibraryRecordBuilder.sanitizeStorageName(
33
+ bundle.symbol.name
34
+ ) +
35
+ '/Data',
36
+ new TextEncoder().encode(
37
+ AltiumLibraryRecordBuilder.buildSchematicComponentRecord(
38
+ bundle
39
+ )
40
+ )
41
+ )
42
+ }
43
+
44
+ return OleCompoundDocumentWriter.write({ streams })
45
+ }
46
+
47
+ /**
48
+ * Normalizes one or more bundles.
49
+ * @param {object[] | object} bundles Bundle input.
50
+ * @returns {object[]}
51
+ */
52
+ static #normalizeBundles(bundles) {
53
+ return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
54
+ SourceComponentBundleNormalizer.normalize(bundle)
55
+ )
56
+ }
57
+ }
@@ -146,17 +146,11 @@ export class AsciiRecordParser {
146
146
  const rawKey = AsciiRecordParser.#trimAscii(
147
147
  segment.slice(0, separatorIndex)
148
148
  )
149
- const value = PrintableTextDecoder.decodeBytes(
150
- AsciiRecordParser.#binaryStringToBytes(
151
- AsciiRecordParser.#trimAscii(
152
- segment.slice(separatorIndex + 1)
153
- )
154
- ),
155
- {
156
- encoding: rawKey.startsWith('%UTF8%') ? 'utf-8' : undefined
157
- }
158
- )
159
149
  const isUtf8Field = rawKey.startsWith('%UTF8%')
150
+ const value = AsciiRecordParser.#decodeFieldValue(
151
+ AsciiRecordParser.#trimAscii(segment.slice(separatorIndex + 1)),
152
+ isUtf8Field ? 'utf-8' : ''
153
+ )
160
154
  const key = rawKey.replace(/^%UTF8%/, '')
161
155
  if (!key) continue
162
156
 
@@ -228,13 +222,51 @@ export class AsciiRecordParser {
228
222
  return normalizedKeyIndex
229
223
  }
230
224
 
225
+ /**
226
+ * Decodes one pipe-delimited field value from the byte-preserving run
227
+ * string. Plain ASCII is already decoded by construction and can avoid
228
+ * byte-array allocation plus TextDecoder fallback probing.
229
+ * @param {string} value Byte-preserving field value.
230
+ * @param {string} preferredEncoding Optional preferred decoder encoding.
231
+ * @returns {string}
232
+ */
233
+ static #decodeFieldValue(value, preferredEncoding) {
234
+ if (!AsciiRecordParser.#hasExtendedByte(value)) {
235
+ return value
236
+ }
237
+
238
+ return PrintableTextDecoder.decodeBytes(
239
+ AsciiRecordParser.#binaryStringToBytes(value),
240
+ { encoding: preferredEncoding || undefined }
241
+ )
242
+ }
243
+
231
244
  /**
232
245
  * Converts one binary string into bytes without altering byte values.
233
246
  * @param {string} value
234
247
  * @returns {Uint8Array}
235
248
  */
236
249
  static #binaryStringToBytes(value) {
237
- return Uint8Array.from(value, (character) => character.charCodeAt(0))
250
+ const bytes = new Uint8Array(value.length)
251
+
252
+ for (let index = 0; index < value.length; index += 1) {
253
+ bytes[index] = value.charCodeAt(index) & 0xff
254
+ }
255
+
256
+ return bytes
257
+ }
258
+
259
+ /**
260
+ * Returns true when the byte-preserving string contains non-ASCII bytes.
261
+ * @param {string} value Field value.
262
+ * @returns {boolean}
263
+ */
264
+ static #hasExtendedByte(value) {
265
+ for (let index = 0; index < value.length; index += 1) {
266
+ if (value.charCodeAt(index) > 0x7f) return true
267
+ }
268
+
269
+ return false
238
270
  }
239
271
 
240
272
  /**
@@ -12,49 +12,49 @@ const KIND_BY_VALUE = {
12
12
  displayName: 'Standard',
13
13
  includeInBom: true,
14
14
  includeInNetlist: true,
15
- includeInPnp: true
15
+ includeInPlacement: true
16
16
  },
17
17
  1: {
18
18
  name: 'mechanical',
19
19
  displayName: 'Mechanical',
20
20
  includeInBom: true,
21
21
  includeInNetlist: true,
22
- includeInPnp: true
22
+ includeInPlacement: true
23
23
  },
24
24
  2: {
25
25
  name: 'graphical',
26
26
  displayName: 'Graphical',
27
27
  includeInBom: false,
28
28
  includeInNetlist: false,
29
- includeInPnp: false
29
+ includeInPlacement: false
30
30
  },
31
31
  3: {
32
32
  name: 'net-tie-bom',
33
33
  displayName: 'Net Tie BOM',
34
34
  includeInBom: true,
35
35
  includeInNetlist: true,
36
- includeInPnp: true
36
+ includeInPlacement: true
37
37
  },
38
38
  4: {
39
39
  name: 'net-tie-no-bom',
40
40
  displayName: 'Net Tie No BOM',
41
41
  includeInBom: false,
42
42
  includeInNetlist: true,
43
- includeInPnp: true
43
+ includeInPlacement: true
44
44
  },
45
45
  5: {
46
46
  name: 'standard-no-bom',
47
47
  displayName: 'Standard No BOM',
48
48
  includeInBom: false,
49
49
  includeInNetlist: true,
50
- includeInPnp: true
50
+ includeInPlacement: true
51
51
  },
52
52
  6: {
53
53
  name: 'jumper',
54
54
  displayName: 'Jumper',
55
55
  includeInBom: true,
56
56
  includeInNetlist: true,
57
- includeInPnp: true
57
+ includeInPlacement: true
58
58
  }
59
59
  }
60
60
 
@@ -65,7 +65,7 @@ export class PcbComponentKindPolicy {
65
65
  /**
66
66
  * Parses native versioned component-kind fields.
67
67
  * @param {Record<string, string | string[]>} fields Native component row.
68
- * @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPnp: boolean } | undefined}
68
+ * @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPlacement: boolean } | undefined}
69
69
  */
70
70
  static parse(fields) {
71
71
  if (!PcbComponentKindPolicy.#hasKindField(fields)) return undefined
@@ -76,7 +76,7 @@ export class PcbComponentKindPolicy {
76
76
  displayName: 'Unknown',
77
77
  includeInBom: true,
78
78
  includeInNetlist: true,
79
- includeInPnp: true
79
+ includeInPlacement: true
80
80
  }
81
81
 
82
82
  return {
@@ -20,14 +20,16 @@ export class PcbEmbeddedModelExtractor {
20
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
21
  */
22
22
  static extractFromStreams(streams) {
23
+ const modelStreamPrefix =
24
+ PcbEmbeddedModelExtractor.#resolveModelStreamPrefix(streams)
23
25
  const modelMetadataRecords =
24
26
  PcbEmbeddedModelExtractor.#parseModelMetadataStream(
25
- streams.get('Models/Data')
27
+ streams.get(modelStreamPrefix + '/Data')
26
28
  )
27
29
  const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
28
30
  fields,
29
31
  index,
30
- sourceStream: 'Models/' + index,
32
+ sourceStream: modelStreamPrefix + '/' + index,
31
33
  id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
32
34
  name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
33
35
  checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
@@ -70,7 +72,24 @@ export class PcbEmbeddedModelExtractor {
70
72
  }
71
73
 
72
74
  /**
73
- * Parses the length-prefixed `Models/Data` metadata stream.
75
+ * Resolves the embedded-model stream folder used by the compound document.
76
+ * @param {Map<string, Uint8Array>} streams
77
+ * @returns {string}
78
+ */
79
+ static #resolveModelStreamPrefix(streams) {
80
+ if (streams.has('Models/Data')) {
81
+ return 'Models'
82
+ }
83
+
84
+ if (streams.has('Library/Models/Data')) {
85
+ return 'Library/Models'
86
+ }
87
+
88
+ return 'Models'
89
+ }
90
+
91
+ /**
92
+ * Parses the length-prefixed model metadata stream.
74
93
  * @param {Uint8Array | undefined} bytes
75
94
  * @returns {Record<string, string | string[]>[]}
76
95
  */
@@ -24,6 +24,8 @@ export class PcbOutlineRecovery {
24
24
 
25
25
  static #MAX_DIRECT_RENDER_ARC_SWEEP_DEGREES = 120
26
26
 
27
+ static #MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO = 16
28
+
27
29
  /**
28
30
  * Selects a recoverable board outline from mechanical track layers.
29
31
  * @param {{ fallbackOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, components: { x: number, y: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerId?: number }[] }} options
@@ -76,6 +78,20 @@ export class PcbOutlineRecovery {
76
78
  }
77
79
  }
78
80
 
81
+ if (
82
+ PcbOutlineRecovery.#shouldKeepAuthoredOutline(
83
+ fallbackOutline,
84
+ boundaryLayer.bounds,
85
+ componentBounds
86
+ )
87
+ ) {
88
+ return {
89
+ source: 'fallback',
90
+ layerId: null,
91
+ outline: fallbackOutline
92
+ }
93
+ }
94
+
79
95
  const recoveredOutline =
80
96
  PcbOutlineRecovery.#traceTrackOutline(
81
97
  boundaryLayer.tracks,
@@ -166,6 +182,84 @@ export class PcbOutlineRecovery {
166
182
  return candidates[0] || null
167
183
  }
168
184
 
185
+ /**
186
+ * Returns true when the mechanical candidate is likely a drawing frame
187
+ * around an otherwise plausible authored board route.
188
+ * @param {{ minX: number, minY: number, widthMil: number, heightMil: number, segments?: Array<Record<string, number | string>> }} authoredOutline Authored board-route outline.
189
+ * @param {{ widthMil: number, heightMil: number }} mechanicalBounds Mechanical track-layer bounds.
190
+ * @param {{ centerX: number, centerY: number }} componentBounds Component placement envelope.
191
+ * @returns {boolean}
192
+ */
193
+ static #shouldKeepAuthoredOutline(
194
+ authoredOutline,
195
+ mechanicalBounds,
196
+ componentBounds
197
+ ) {
198
+ if (
199
+ !PcbOutlineRecovery.#isClosedOutlinePath(
200
+ authoredOutline?.segments || []
201
+ )
202
+ ) {
203
+ return false
204
+ }
205
+
206
+ const authoredArea =
207
+ Number(authoredOutline?.widthMil || 0) *
208
+ Number(authoredOutline?.heightMil || 0)
209
+ const mechanicalArea =
210
+ Number(mechanicalBounds?.widthMil || 0) *
211
+ Number(mechanicalBounds?.heightMil || 0)
212
+
213
+ if (!authoredArea || !mechanicalArea) {
214
+ return false
215
+ }
216
+
217
+ if (
218
+ mechanicalArea / authoredArea <=
219
+ PcbOutlineRecovery.#MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO
220
+ ) {
221
+ return false
222
+ }
223
+
224
+ return PcbOutlineRecovery.#outlineEnvelopeContainsPoint(
225
+ authoredOutline,
226
+ componentBounds?.centerX,
227
+ componentBounds?.centerY
228
+ )
229
+ }
230
+
231
+ /**
232
+ * Returns true when one point falls within an outline's bounding envelope.
233
+ * @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number }} outline Outline bounds.
234
+ * @param {number} x Point X coordinate.
235
+ * @param {number} y Point Y coordinate.
236
+ * @returns {boolean}
237
+ */
238
+ static #outlineEnvelopeContainsPoint(outline, x, y) {
239
+ const minX = Number(outline?.minX)
240
+ const minY = Number(outline?.minY)
241
+ const widthMil = Number(outline?.widthMil)
242
+ const heightMil = Number(outline?.heightMil)
243
+
244
+ if (
245
+ !Number.isFinite(minX) ||
246
+ !Number.isFinite(minY) ||
247
+ !Number.isFinite(widthMil) ||
248
+ !Number.isFinite(heightMil) ||
249
+ !Number.isFinite(x) ||
250
+ !Number.isFinite(y)
251
+ ) {
252
+ return false
253
+ }
254
+
255
+ return (
256
+ x >= minX &&
257
+ x <= minX + widthMil &&
258
+ y >= minY &&
259
+ y <= minY + heightMil
260
+ )
261
+ }
262
+
169
263
  /**
170
264
  * Builds one track-bounds envelope.
171
265
  * @param {{ x1: number, y1: number, x2: number, y2: number }[]} tracks
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { ParserUtils } from './ParserUtils.mjs'
6
+ import { SchematicNoErcSymbolResolver } from './SchematicNoErcSymbolResolver.mjs'
6
7
 
7
8
  /**
8
9
  * Helpers for normalized schematic directive primitives.
@@ -144,6 +145,7 @@ export class SchematicDirectiveParser {
144
145
  const x = ParserUtils.parseNumericField(record.fields, 'Location.X')
145
146
  const y = ParserUtils.parseNumericField(record.fields, 'Location.Y')
146
147
  if (x === null || y === null) return null
148
+ const rawSymbol = ParserUtils.getField(record.fields, 'Symbol')
147
149
  const symbol = ParserUtils.parseNumericField(record.fields, 'Symbol')
148
150
 
149
151
  return {
@@ -160,7 +162,9 @@ export class SchematicDirectiveParser {
160
162
  ParserUtils.parseNumericField(record.fields, 'Orientation') ||
161
163
  0,
162
164
  symbol,
163
- symbolName: SchematicDirectiveParser.#noErcSymbolName(symbol)
165
+ symbolName: SchematicNoErcSymbolResolver.resolveSymbolName(
166
+ rawSymbol || symbol
167
+ )
164
168
  }
165
169
  }
166
170
 
@@ -332,22 +336,6 @@ export class SchematicDirectiveParser {
332
336
  return 'record-' + String(record.recordIndex ?? 0)
333
337
  }
334
338
 
335
- /**
336
- * Converts common No ERC symbol ids into public labels.
337
- * @param {number | null} symbol Symbol id.
338
- * @returns {string}
339
- */
340
- static #noErcSymbolName(symbol) {
341
- return (
342
- {
343
- 0: 'generic',
344
- 1: 'box',
345
- 2: 'cross',
346
- 3: 'triangle'
347
- }[Number(symbol)] || 'unknown'
348
- )
349
- }
350
-
351
339
  /**
352
340
  * Parses one boolean-ish directive parameter value.
353
341
  * @param {unknown} value Raw parameter value.
@@ -0,0 +1,36 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Resolves Altium No ERC marker symbol identifiers into stable names.
7
+ */
8
+ export class SchematicNoErcSymbolResolver {
9
+ /**
10
+ * Converts common No ERC symbol ids and names into public labels.
11
+ * @param {number | string | null | undefined} symbol Symbol id or source name.
12
+ * @returns {string}
13
+ */
14
+ static resolveSymbolName(symbol) {
15
+ const text = String(symbol ?? '')
16
+ .trim()
17
+ .toLowerCase()
18
+
19
+ if (text) {
20
+ if (/check\s*box/.test(text)) return 'checkbox'
21
+ if (/cross/.test(text)) return 'cross'
22
+ if (/triangle/.test(text)) return 'triangle'
23
+ if (/box/.test(text)) return 'box'
24
+ if (/generic/.test(text)) return 'generic'
25
+ }
26
+
27
+ return (
28
+ {
29
+ 0: 'generic',
30
+ 1: 'box',
31
+ 2: 'cross',
32
+ 3: 'triangle'
33
+ }[Number(symbol)] || 'unknown'
34
+ )
35
+ }
36
+ }