altium-toolkit 0.1.20 → 0.1.21
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 +1 -1
- package/src/core/altium/AltiumParser.mjs +15 -3
- package/src/core/altium/PcbFontMetricsParser.mjs +33 -6
- package/src/core/altium/PcbModelParser.mjs +82 -0
- package/src/core/altium/PcbTextPrimitiveParser.mjs +48 -3
- package/src/core/altium/SchematicPinDesignatorInferer.mjs +401 -0
- package/src/core/altium/SchematicPinParser.mjs +136 -56
- package/src/core/altium/SchematicStreamExtractor.mjs +93 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +40 -12
- package/src/ui/PcbScene3dBuilder.mjs +593 -53
- package/src/ui/PcbScene3dDrillCutoutBuilder.mjs +453 -0
- package/src/ui/SchematicImageRenderer.mjs +134 -32
- package/src/ui/SchematicJunctionRenderer.mjs +22 -4
- package/src/ui/SchematicNoteRenderer.mjs +73 -9
- package/src/ui/SchematicPinSvgRenderer.mjs +25 -3
- package/src/ui/SchematicPowerPortRenderer.mjs +183 -36
- package/src/ui/SchematicSvgRenderer.mjs +2 -2
package/package.json
CHANGED
|
@@ -26,6 +26,7 @@ import { SchematicBusEntryParser } from './SchematicBusEntryParser.mjs'
|
|
|
26
26
|
import { SchematicImageParser } from './SchematicImageParser.mjs'
|
|
27
27
|
import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
|
|
28
28
|
import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
|
|
29
|
+
import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
|
|
29
30
|
const {
|
|
30
31
|
countMatchingKeys,
|
|
31
32
|
getDisplayText,
|
|
@@ -65,7 +66,13 @@ export class AltiumParser {
|
|
|
65
66
|
const records = AsciiRecordParser.parse(arrayBuffer)
|
|
66
67
|
const fileType = AltiumParser.#sniffFileType(fileName, records)
|
|
67
68
|
if (fileType === 'SchDoc') {
|
|
68
|
-
|
|
69
|
+
const schematicExtraction =
|
|
70
|
+
SchematicStreamExtractor.extractFromArrayBuffer(arrayBuffer)
|
|
71
|
+
return AltiumParser.#parseSchematic(
|
|
72
|
+
fileName,
|
|
73
|
+
schematicExtraction?.records || records,
|
|
74
|
+
arrayBuffer
|
|
75
|
+
)
|
|
69
76
|
}
|
|
70
77
|
if (fileType === 'PcbDoc') {
|
|
71
78
|
const pcbExtraction =
|
|
@@ -270,13 +277,15 @@ export class AltiumParser {
|
|
|
270
277
|
color: toColor(record.fields.Color, '#a44a1b'),
|
|
271
278
|
width: parseNumericField(record.fields, 'LineWidth') || 1,
|
|
272
279
|
lineStyle: parseNumericField(record.fields, 'LineStyle') || 0,
|
|
280
|
+
recordType: getField(record.fields, 'RECORD') || undefined,
|
|
273
281
|
renderOrder:
|
|
274
282
|
parseNumericField(record.fields, 'IndexInSheet') ?? index,
|
|
275
283
|
ownerIndex: getField(record.fields, 'OwnerIndex') || undefined
|
|
276
284
|
})),
|
|
277
285
|
...polylineRecords.flatMap((record, index) =>
|
|
278
286
|
parseSchematicPolyline(record.fields, {
|
|
279
|
-
isBus: getField(record.fields, 'RECORD') === '26'
|
|
287
|
+
isBus: getField(record.fields, 'RECORD') === '26',
|
|
288
|
+
recordType: getField(record.fields, 'RECORD') || undefined
|
|
280
289
|
}).map((line, segmentIndex) => ({
|
|
281
290
|
...line,
|
|
282
291
|
renderOrder:
|
|
@@ -291,6 +300,8 @@ export class AltiumParser {
|
|
|
291
300
|
parseSchematicPolygon(record.fields).map(
|
|
292
301
|
(line, segmentIndex) => ({
|
|
293
302
|
...line,
|
|
303
|
+
recordType:
|
|
304
|
+
getField(record.fields, 'RECORD') || undefined,
|
|
294
305
|
renderOrder:
|
|
295
306
|
(parseNumericField(record.fields, 'IndexInSheet') ??
|
|
296
307
|
index) +
|
|
@@ -379,7 +390,8 @@ export class AltiumParser {
|
|
|
379
390
|
texts,
|
|
380
391
|
normalizedLines,
|
|
381
392
|
pins,
|
|
382
|
-
ports
|
|
393
|
+
ports,
|
|
394
|
+
rectangles
|
|
383
395
|
),
|
|
384
396
|
normalizedLines,
|
|
385
397
|
pins,
|
|
@@ -9,7 +9,7 @@ export class PcbFontMetricsParser {
|
|
|
9
9
|
/**
|
|
10
10
|
* Parses font-family metrics from a TrueType/OpenType sfnt payload.
|
|
11
11
|
* @param {Uint8Array | ArrayBuffer} bytes
|
|
12
|
-
* @returns {{ format: 'truetype' | 'opentype' | 'unknown', unitsPerEm?: number, ascent?: number, descent?: number, lineGap?: number, cellHeight?: number, emScaleFromPcbHeight?: number, capHeight?: number, averageAdvanceWidth?: number, weightClass?: number, widthClass?: number }}
|
|
12
|
+
* @returns {{ format: 'truetype' | 'opentype' | 'unknown', unitsPerEm?: number, ascent?: number, descent?: number, lineGap?: number, windowsAscent?: number, windowsDescent?: number, cellHeight?: number, emScaleFromPcbHeight?: number, capHeight?: number, averageAdvanceWidth?: number, weightClass?: number, widthClass?: number }}
|
|
13
13
|
*/
|
|
14
14
|
static parse(bytes) {
|
|
15
15
|
const normalizedBytes = PcbFontMetricsParser.#toUint8Array(bytes)
|
|
@@ -43,10 +43,7 @@ export class PcbFontMetricsParser {
|
|
|
43
43
|
tables.get('hmtx'),
|
|
44
44
|
hhea.numberOfHMetrics
|
|
45
45
|
)
|
|
46
|
-
const cellHeight =
|
|
47
|
-
Number.isFinite(hhea.ascent) && Number.isFinite(hhea.descent)
|
|
48
|
-
? hhea.ascent + Math.abs(hhea.descent)
|
|
49
|
-
: undefined
|
|
46
|
+
const cellHeight = PcbFontMetricsParser.#resolvePcbCellHeight(hhea, os2)
|
|
50
47
|
const metrics = {
|
|
51
48
|
format,
|
|
52
49
|
...head,
|
|
@@ -177,7 +174,7 @@ export class PcbFontMetricsParser {
|
|
|
177
174
|
* Reads typography metadata from the `OS/2` table.
|
|
178
175
|
* @param {DataView} view
|
|
179
176
|
* @param {{ offset: number, length: number } | undefined} table
|
|
180
|
-
* @returns {{ averageAdvanceWidth?: number, weightClass?: number, widthClass?: number, capHeight?: number }}
|
|
177
|
+
* @returns {{ averageAdvanceWidth?: number, weightClass?: number, widthClass?: number, windowsAscent?: number, windowsDescent?: number, capHeight?: number }}
|
|
181
178
|
*/
|
|
182
179
|
static #readOs2Table(view, table) {
|
|
183
180
|
if (!PcbFontMetricsParser.#tableHasBytes(table, 8)) {
|
|
@@ -203,10 +200,40 @@ export class PcbFontMetricsParser {
|
|
|
203
200
|
table.offset + 88
|
|
204
201
|
)
|
|
205
202
|
}
|
|
203
|
+
if (PcbFontMetricsParser.#tableHasBytes(table, 78)) {
|
|
204
|
+
metrics.windowsAscent = PcbFontMetricsParser.#readUint16(
|
|
205
|
+
view,
|
|
206
|
+
table.offset + 74
|
|
207
|
+
)
|
|
208
|
+
metrics.windowsDescent = PcbFontMetricsParser.#readUint16(
|
|
209
|
+
view,
|
|
210
|
+
table.offset + 76
|
|
211
|
+
)
|
|
212
|
+
}
|
|
206
213
|
|
|
207
214
|
return metrics
|
|
208
215
|
}
|
|
209
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Resolves the TrueType cell height used by Altium PCB text placement.
|
|
219
|
+
* @param {{ ascent?: number, descent?: number }} hhea
|
|
220
|
+
* @param {{ windowsAscent?: number, windowsDescent?: number }} os2
|
|
221
|
+
* @returns {number | undefined}
|
|
222
|
+
*/
|
|
223
|
+
static #resolvePcbCellHeight(hhea, os2) {
|
|
224
|
+
if (
|
|
225
|
+
Number.isFinite(os2.windowsAscent) &&
|
|
226
|
+
Number.isFinite(os2.windowsDescent) &&
|
|
227
|
+
os2.windowsAscent + os2.windowsDescent > 0
|
|
228
|
+
) {
|
|
229
|
+
return os2.windowsAscent + os2.windowsDescent
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Number.isFinite(hhea.ascent) && Number.isFinite(hhea.descent)
|
|
233
|
+
? hhea.ascent + Math.abs(hhea.descent)
|
|
234
|
+
: undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
/**
|
|
211
238
|
* Reads horizontal advance data from the `hmtx` table.
|
|
212
239
|
* @param {DataView} view
|
|
@@ -93,6 +93,7 @@ export class PcbModelParser {
|
|
|
93
93
|
const primitiveLayers = AltiumLayoutParser.parsePrimitiveLayerNames(
|
|
94
94
|
boardRecords.map((record) => record.fields)
|
|
95
95
|
)
|
|
96
|
+
const appearance3d = PcbModelParser.#parseAppearance3d(boardRecords)
|
|
96
97
|
const nets = PcbModelParser.#parseNetRecords(records)
|
|
97
98
|
const netNameByIndex = PcbModelParser.#buildNetNameMap(nets)
|
|
98
99
|
const classes = PcbModelParser.#parseClassRecords(records)
|
|
@@ -392,6 +393,7 @@ export class PcbModelParser {
|
|
|
392
393
|
layerSubstacks,
|
|
393
394
|
boardRegionContexts,
|
|
394
395
|
primitiveLayers,
|
|
396
|
+
appearance3d,
|
|
395
397
|
nets,
|
|
396
398
|
classes,
|
|
397
399
|
rules,
|
|
@@ -558,6 +560,86 @@ export class PcbModelParser {
|
|
|
558
560
|
return netNameByIndex
|
|
559
561
|
}
|
|
560
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Extracts authored Altium 3D appearance colors from board metadata.
|
|
565
|
+
* @param {{ fields: Record<string, string | string[]> }[]} boardRecords
|
|
566
|
+
* @returns {{ boardCoreColor?: number, solderMaskTopColor?: number, solderMaskBottomColor?: number, copperColor?: number, silkscreenTopColor?: number, silkscreenBottomColor?: number } | null}
|
|
567
|
+
*/
|
|
568
|
+
static #parseAppearance3d(boardRecords) {
|
|
569
|
+
const configText = (Array.isArray(boardRecords) ? boardRecords : [])
|
|
570
|
+
.map((record) => getField(record.fields, '3DCONFIGURATION'))
|
|
571
|
+
.find(Boolean)
|
|
572
|
+
if (!configText) {
|
|
573
|
+
return null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const config = PcbModelParser.#parseConfigurationFields(configText)
|
|
577
|
+
const appearance = {
|
|
578
|
+
boardCoreColor: PcbModelParser.#parseAltiumBgrColor(
|
|
579
|
+
config.get('CFG3D.BOARDCORECOLOR')
|
|
580
|
+
),
|
|
581
|
+
solderMaskTopColor: PcbModelParser.#parseAltiumBgrColor(
|
|
582
|
+
config.get('CFG3D.TOPSOLDERMASKCOLOR')
|
|
583
|
+
),
|
|
584
|
+
solderMaskBottomColor: PcbModelParser.#parseAltiumBgrColor(
|
|
585
|
+
config.get('CFG3D.BOTSOLDERMASKCOLOR')
|
|
586
|
+
),
|
|
587
|
+
copperColor: PcbModelParser.#parseAltiumBgrColor(
|
|
588
|
+
config.get('CFG3D.COPPERCOLOR')
|
|
589
|
+
),
|
|
590
|
+
silkscreenTopColor: PcbModelParser.#parseAltiumBgrColor(
|
|
591
|
+
config.get('CFG3D.TOPSILKSCREENCOLOR')
|
|
592
|
+
),
|
|
593
|
+
silkscreenBottomColor: PcbModelParser.#parseAltiumBgrColor(
|
|
594
|
+
config.get('CFG3D.BOTSILKSCREENCOLOR')
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return Object.values(appearance).some(Number.isInteger)
|
|
599
|
+
? appearance
|
|
600
|
+
: null
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Parses backtick-delimited Altium configuration key/value fields.
|
|
605
|
+
* @param {string} value
|
|
606
|
+
* @returns {Map<string, string>}
|
|
607
|
+
*/
|
|
608
|
+
static #parseConfigurationFields(value) {
|
|
609
|
+
const fields = new Map()
|
|
610
|
+
|
|
611
|
+
String(value || '')
|
|
612
|
+
.split('`')
|
|
613
|
+
.forEach((segment) => {
|
|
614
|
+
const separatorIndex = segment.indexOf('=')
|
|
615
|
+
if (separatorIndex <= 0) {
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
fields.set(
|
|
620
|
+
segment.slice(0, separatorIndex).toUpperCase(),
|
|
621
|
+
segment.slice(separatorIndex + 1)
|
|
622
|
+
)
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
return fields
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Converts Altium decimal BGR color storage into an RGB integer.
|
|
630
|
+
* @param {string | undefined} value
|
|
631
|
+
* @returns {number | undefined}
|
|
632
|
+
*/
|
|
633
|
+
static #parseAltiumBgrColor(value) {
|
|
634
|
+
const parsed = Number.parseInt(String(value ?? '').trim(), 10)
|
|
635
|
+
if (!Number.isFinite(parsed)) {
|
|
636
|
+
return undefined
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const bgr = parsed & 0xffffff
|
|
640
|
+
return ((bgr & 0xff) << 16) | (bgr & 0xff00) | (bgr >> 16)
|
|
641
|
+
}
|
|
642
|
+
|
|
561
643
|
/**
|
|
562
644
|
* Adds resolved net names to decoded primitives without changing geometry.
|
|
563
645
|
* @param {{ netIndex?: number | string | null }[]} primitives
|
|
@@ -17,7 +17,7 @@ export class PcbTextPrimitiveParser {
|
|
|
17
17
|
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
18
18
|
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
19
19
|
* @param {{ wideStrings?: Map<number | string, string> | Record<string, string> | { byIndex?: Record<string, string> } }} [options]
|
|
20
|
-
* @returns {{ text: string, x: number, y: number, height: number, layerId: number, ownerIndex: number | null, kind: number, visibilityFlags: number, rotation: number, strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, wideStringIndex?: number, textSource?: string, role?: string, isDesignator?: boolean, isComment?: boolean, isPlaceholder?: boolean, componentIndex?: number }[]}
|
|
20
|
+
* @returns {{ text: string, x: number, y: number, height: number, layerId: number, ownerIndex: number | null, kind: number, visibilityFlags: number, rotation: number, mirrored: boolean, strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, isInverted?: boolean, marginBorderWidth?: number, wideStringIndex?: number, useInvertedRectangle?: boolean, textboxRectWidth?: number, textboxRectHeight?: number, textboxRectJustification?: number, textSource?: string, role?: string, isDesignator?: boolean, isComment?: boolean, isPlaceholder?: boolean, componentIndex?: number }[]}
|
|
21
21
|
*/
|
|
22
22
|
static parseTextStream(headerBytes, dataBytes, options = {}) {
|
|
23
23
|
const count = PcbTextPrimitiveParser.#readRecordCount(headerBytes)
|
|
@@ -106,7 +106,7 @@ export class PcbTextPrimitiveParser {
|
|
|
106
106
|
* @param {DataView} payload
|
|
107
107
|
* @param {Uint8Array} textBytes
|
|
108
108
|
* @param {Map<string, string>} wideStrings
|
|
109
|
-
* @returns {{ text: string, x: number, y: number, height: number, layerId: number, ownerIndex: number | null, kind: number, visibilityFlags: number, rotation: number, strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, wideStringIndex?: number, textSource?: string, role?: string, isDesignator?: boolean, isComment?: boolean, isPlaceholder?: boolean, componentIndex?: number } | null}
|
|
109
|
+
* @returns {{ text: string, x: number, y: number, height: number, layerId: number, ownerIndex: number | null, kind: number, visibilityFlags: number, rotation: number, mirrored: boolean, strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, isInverted?: boolean, marginBorderWidth?: number, wideStringIndex?: number, useInvertedRectangle?: boolean, textboxRectWidth?: number, textboxRectHeight?: number, textboxRectJustification?: number, textSource?: string, role?: string, isDesignator?: boolean, isComment?: boolean, isPlaceholder?: boolean, componentIndex?: number } | null}
|
|
110
110
|
*/
|
|
111
111
|
static #parseTextRecord(payload, textBytes, wideStrings) {
|
|
112
112
|
if (
|
|
@@ -157,6 +157,10 @@ export class PcbTextPrimitiveParser {
|
|
|
157
157
|
visibilityFlags,
|
|
158
158
|
hasExtendedFontFields
|
|
159
159
|
),
|
|
160
|
+
mirrored: PcbTextPrimitiveParser.#resolveTextMirrored(
|
|
161
|
+
payload,
|
|
162
|
+
hasExtendedFontFields
|
|
163
|
+
),
|
|
160
164
|
...extendedText,
|
|
161
165
|
...resolvedText.metadata,
|
|
162
166
|
...role,
|
|
@@ -171,7 +175,7 @@ export class PcbTextPrimitiveParser {
|
|
|
171
175
|
* Parses extended TrueType/barcode font metadata when the payload carries
|
|
172
176
|
* the modern PCB text field block.
|
|
173
177
|
* @param {DataView} payload
|
|
174
|
-
* @returns {{ strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, wideStringIndex?: number }}
|
|
178
|
+
* @returns {{ strokeFontType?: number, strokeWidth?: number, fontType?: number, fontTypeName?: string, fontName?: string, fontFamily?: string, isBold?: boolean, isItalic?: boolean, fontWeight?: number, fontStyle?: string, isInverted?: boolean, marginBorderWidth?: number, wideStringIndex?: number, useInvertedRectangle?: boolean, textboxRectWidth?: number, textboxRectHeight?: number, textboxRectJustification?: number }}
|
|
175
179
|
*/
|
|
176
180
|
static #parseExtendedTextFields(payload) {
|
|
177
181
|
if (payload.byteLength < 110) {
|
|
@@ -201,9 +205,36 @@ export class PcbTextPrimitiveParser {
|
|
|
201
205
|
fontStyle: isItalic ? 'italic' : 'normal'
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
if (payload.byteLength >= 111) {
|
|
209
|
+
extendedFields.isInverted = payload.getUint8(110) !== 0
|
|
210
|
+
}
|
|
211
|
+
if (payload.byteLength >= 115) {
|
|
212
|
+
extendedFields.marginBorderWidth = PcbTextPrimitiveParser.#readMil(
|
|
213
|
+
payload,
|
|
214
|
+
111
|
|
215
|
+
)
|
|
216
|
+
}
|
|
204
217
|
if (payload.byteLength >= 119) {
|
|
205
218
|
extendedFields.wideStringIndex = payload.getUint32(115, true)
|
|
206
219
|
}
|
|
220
|
+
if (payload.byteLength >= 124) {
|
|
221
|
+
extendedFields.useInvertedRectangle = payload.getUint8(123) !== 0
|
|
222
|
+
}
|
|
223
|
+
if (payload.byteLength >= 128) {
|
|
224
|
+
extendedFields.textboxRectWidth = PcbTextPrimitiveParser.#readMil(
|
|
225
|
+
payload,
|
|
226
|
+
124
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
if (payload.byteLength >= 132) {
|
|
230
|
+
extendedFields.textboxRectHeight = PcbTextPrimitiveParser.#readMil(
|
|
231
|
+
payload,
|
|
232
|
+
128
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
if (payload.byteLength >= 133) {
|
|
236
|
+
extendedFields.textboxRectJustification = payload.getUint8(132)
|
|
237
|
+
}
|
|
207
238
|
|
|
208
239
|
return extendedFields
|
|
209
240
|
}
|
|
@@ -455,6 +486,20 @@ export class PcbTextPrimitiveParser {
|
|
|
455
486
|
return PcbTextPrimitiveParser.#textRotationFromFlags(visibilityFlags)
|
|
456
487
|
}
|
|
457
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Resolves text mirroring from the explicit modern text byte.
|
|
491
|
+
* @param {DataView} payload
|
|
492
|
+
* @param {boolean} hasExtendedFontFields
|
|
493
|
+
* @returns {boolean}
|
|
494
|
+
*/
|
|
495
|
+
static #resolveTextMirrored(payload, hasExtendedFontFields) {
|
|
496
|
+
if (hasExtendedFontFields && payload.byteLength >= 36) {
|
|
497
|
+
return payload.getUint8(35) !== 0
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return false
|
|
501
|
+
}
|
|
502
|
+
|
|
458
503
|
/**
|
|
459
504
|
* Normalizes supported WideStrings6 lookup shapes into a string-keyed map.
|
|
460
505
|
* @param {Map<number | string, string> | Record<string, string> | { byIndex?: Record<string, string> } | undefined} wideStrings
|