altium-toolkit 0.1.0
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +20 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +22 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/GPL-3.0-or-later.txt +232 -0
- package/NOTICE.md +32 -0
- package/README.md +116 -0
- package/docs/api.md +73 -0
- package/docs/model-format.md +36 -0
- package/docs/testing.md +25 -0
- package/examples/README.md +47 -0
- package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
- package/examples/arduino-uno/SvgViewportController.mjs +306 -0
- package/examples/arduino-uno/example.mjs +480 -0
- package/examples/arduino-uno/index.html +163 -0
- package/examples/arduino-uno/styles.css +552 -0
- package/examples/server.mjs +212 -0
- package/package.json +53 -0
- package/spec/library-scope.md +32 -0
- package/src/core/BinaryReader.mjs +127 -0
- package/src/core/altium/AltiumLayoutParser.mjs +485 -0
- package/src/core/altium/AltiumParser.mjs +1007 -0
- package/src/core/altium/AsciiRecordParser.mjs +151 -0
- package/src/core/altium/ParserUtils.mjs +173 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
- package/src/core/altium/PcbModelParser.mjs +336 -0
- package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
- package/src/core/altium/PcbStreamExtractor.mjs +210 -0
- package/src/core/altium/PrintableTextDecoder.mjs +156 -0
- package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
- package/src/core/altium/SchematicImageParser.mjs +173 -0
- package/src/core/altium/SchematicJunctionParser.mjs +43 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
- package/src/core/altium/SchematicPinParser.mjs +767 -0
- package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
- package/src/core/altium/SchematicSheetParser.mjs +241 -0
- package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
- package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
- package/src/core/altium/SchematicTextParser.mjs +708 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
- package/src/core/ole/OleCompoundDocument.mjs +439 -0
- package/src/core/ole/OleConstants.mjs +64 -0
- package/src/core/ole/OleDirectoryEntry.mjs +95 -0
- package/src/index.mjs +7 -0
- package/src/parser.mjs +21 -0
- package/src/renderers.mjs +15 -0
- package/src/scene3d.mjs +9 -0
- package/src/styles/altium-renderers.css +358 -0
- package/src/ui/BomTableRenderer.mjs +46 -0
- package/src/ui/PcbArcUtils.mjs +189 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
- package/src/ui/PcbScene3dBuilder.mjs +742 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
- package/src/ui/PcbScene3dPackages.mjs +137 -0
- package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
- package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
- package/src/ui/PcbSvgRenderer.mjs +906 -0
- package/src/ui/SchematicColorResolver.mjs +132 -0
- package/src/ui/SchematicContentLayout.mjs +661 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
- package/src/ui/SchematicImageRenderer.mjs +135 -0
- package/src/ui/SchematicJunctionRenderer.mjs +381 -0
- package/src/ui/SchematicNoteRenderer.mjs +427 -0
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
- package/src/ui/SchematicPortRenderer.mjs +558 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
- package/src/ui/SchematicRegionRenderer.mjs +94 -0
- package/src/ui/SchematicShapeRenderer.mjs +398 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
- package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
- package/src/ui/SchematicSvgRenderer.mjs +756 -0
- package/src/ui/SchematicSvgUtils.mjs +182 -0
- package/src/ui/SchematicTypography.mjs +204 -0
- package/src/workers/altium-parser.worker.mjs +29 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getField, parseBoolean, parseNumericField, toColor } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes schematic sheet symbols and sheet entries.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicSheetParser {
|
|
13
|
+
/**
|
|
14
|
+
* Parses sheet symbols and their child entries from schematic records.
|
|
15
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
|
|
16
|
+
* @returns {{ sheetSymbols: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null }[], sheetEntries: { ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number }[] }}
|
|
17
|
+
*/
|
|
18
|
+
static parse(records) {
|
|
19
|
+
const sheetSymbols = records
|
|
20
|
+
.map((record) =>
|
|
21
|
+
SchematicSheetParser.#parseSheetSymbolRecord(record)
|
|
22
|
+
)
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
const symbolLookup =
|
|
25
|
+
SchematicSheetParser.#buildSheetSymbolLookup(sheetSymbols)
|
|
26
|
+
const sheetEntries = records
|
|
27
|
+
.map((record) =>
|
|
28
|
+
SchematicSheetParser.#parseSheetEntryRecord(
|
|
29
|
+
record,
|
|
30
|
+
symbolLookup
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
|
|
35
|
+
return { sheetSymbols, sheetEntries }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalizes one `RECORD=15` sheet symbol.
|
|
40
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
|
|
41
|
+
* @returns {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null } | null}
|
|
42
|
+
*/
|
|
43
|
+
static #parseSheetSymbolRecord(record) {
|
|
44
|
+
if (getField(record.fields, 'RECORD') !== '15') {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const x = parseNumericField(record.fields, 'Location.X')
|
|
49
|
+
const y = parseNumericField(record.fields, 'Location.Y')
|
|
50
|
+
const width = parseNumericField(record.fields, 'XSize')
|
|
51
|
+
const height = parseNumericField(record.fields, 'YSize')
|
|
52
|
+
|
|
53
|
+
if (x === null || y === null || width === null || height === null) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
x,
|
|
61
|
+
y,
|
|
62
|
+
width,
|
|
63
|
+
height,
|
|
64
|
+
color: toColor(record.fields.Color, '#a44a1b'),
|
|
65
|
+
fill: toColor(record.fields.AreaColor, '#ffe16f'),
|
|
66
|
+
isSolid: parseBoolean(record.fields.IsSolid),
|
|
67
|
+
transparent: parseBoolean(record.fields.Transparent),
|
|
68
|
+
ownerIndex: getField(record.fields, 'OwnerIndex') || undefined,
|
|
69
|
+
uniqueId: getField(record.fields, 'UniqueId'),
|
|
70
|
+
renderOrder: indexInSheet ?? record.recordIndex,
|
|
71
|
+
sourceRecordIndex: record.recordIndex,
|
|
72
|
+
indexInSheet
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds a lookup that tolerates the owner-index variants found in
|
|
78
|
+
* recovered Altium records.
|
|
79
|
+
* @param {{ sourceRecordIndex: number, indexInSheet: number | null }[]} sheetSymbols
|
|
80
|
+
* @returns {Map<string, any>}
|
|
81
|
+
*/
|
|
82
|
+
static #buildSheetSymbolLookup(sheetSymbols) {
|
|
83
|
+
const lookup = new Map()
|
|
84
|
+
|
|
85
|
+
for (const sheetSymbol of sheetSymbols) {
|
|
86
|
+
const candidateKeys = new Set([
|
|
87
|
+
String(sheetSymbol.sourceRecordIndex),
|
|
88
|
+
String(sheetSymbol.sourceRecordIndex + 1)
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
if (sheetSymbol.indexInSheet !== null) {
|
|
92
|
+
candidateKeys.add(String(sheetSymbol.indexInSheet))
|
|
93
|
+
candidateKeys.add(String(sheetSymbol.indexInSheet + 1))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const key of candidateKeys) {
|
|
97
|
+
lookup.set(key, sheetSymbol)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lookup
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Normalizes one `RECORD=16` sheet entry against its parent symbol.
|
|
106
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
|
|
107
|
+
* @param {Map<string, { x: number, y: number, width: number, height: number }>} symbolLookup
|
|
108
|
+
* @returns {{ ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number } | null}
|
|
109
|
+
*/
|
|
110
|
+
static #parseSheetEntryRecord(record, symbolLookup) {
|
|
111
|
+
if (getField(record.fields, 'RECORD') !== '16') {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ownerIndex = getField(record.fields, 'OwnerIndex')
|
|
116
|
+
const name = getField(record.fields, 'Name')
|
|
117
|
+
const parentSymbol = symbolLookup.get(ownerIndex)
|
|
118
|
+
|
|
119
|
+
if (!ownerIndex || !name || !parentSymbol) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const side = SchematicSheetParser.#resolveSheetEntrySide(
|
|
124
|
+
parseNumericField(record.fields, 'Side')
|
|
125
|
+
)
|
|
126
|
+
const distance = SchematicSheetParser.#parseSheetEntryDistance(
|
|
127
|
+
record.fields
|
|
128
|
+
)
|
|
129
|
+
const point = SchematicSheetParser.#resolveSheetEntryPoint(
|
|
130
|
+
parentSymbol,
|
|
131
|
+
side,
|
|
132
|
+
distance
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ownerIndex,
|
|
137
|
+
name,
|
|
138
|
+
side,
|
|
139
|
+
direction: SchematicSheetParser.#resolveSheetEntryDirection(
|
|
140
|
+
parseNumericField(record.fields, 'IOType')
|
|
141
|
+
),
|
|
142
|
+
style: parseNumericField(record.fields, 'Style') || 0,
|
|
143
|
+
x: point.x,
|
|
144
|
+
y: point.y,
|
|
145
|
+
color: toColor(record.fields.Color, '#a44a1b'),
|
|
146
|
+
fill: toColor(record.fields.AreaColor, '#ffe16f'),
|
|
147
|
+
textColor: toColor(
|
|
148
|
+
record.fields.TextColor || record.fields.Color,
|
|
149
|
+
'#2c3134'
|
|
150
|
+
),
|
|
151
|
+
harnessType: getField(record.fields, 'HarnessType'),
|
|
152
|
+
renderOrder:
|
|
153
|
+
parseNumericField(record.fields, 'IndexInSheet') ??
|
|
154
|
+
record.recordIndex
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parses Altium's `DistanceFromTop` plus optional fractional companion.
|
|
160
|
+
* @param {Record<string, string | string[]>} fields
|
|
161
|
+
* @returns {number}
|
|
162
|
+
*/
|
|
163
|
+
static #parseSheetEntryDistance(fields) {
|
|
164
|
+
const whole = parseNumericField(fields, 'DistanceFromTop') || 0
|
|
165
|
+
const fraction = parseNumericField(fields, 'DistanceFromTop_FRAC1') || 0
|
|
166
|
+
const sign = whole < 0 ? -1 : 1
|
|
167
|
+
|
|
168
|
+
return whole * 10 + (fraction / 100000) * sign
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolves one entry point on the parent sheet symbol perimeter.
|
|
173
|
+
* @param {{ x: number, y: number, width: number, height: number }} parentSymbol
|
|
174
|
+
* @param {'left' | 'right' | 'top' | 'bottom'} side
|
|
175
|
+
* @param {number} distance
|
|
176
|
+
* @returns {{ x: number, y: number }}
|
|
177
|
+
*/
|
|
178
|
+
static #resolveSheetEntryPoint(parentSymbol, side, distance) {
|
|
179
|
+
switch (side) {
|
|
180
|
+
case 'right':
|
|
181
|
+
return {
|
|
182
|
+
x: parentSymbol.x + parentSymbol.width,
|
|
183
|
+
y: parentSymbol.y - distance
|
|
184
|
+
}
|
|
185
|
+
case 'top':
|
|
186
|
+
return {
|
|
187
|
+
x: parentSymbol.x + distance,
|
|
188
|
+
y: parentSymbol.y
|
|
189
|
+
}
|
|
190
|
+
case 'bottom':
|
|
191
|
+
return {
|
|
192
|
+
x: parentSymbol.x + distance,
|
|
193
|
+
y: parentSymbol.y - parentSymbol.height
|
|
194
|
+
}
|
|
195
|
+
case 'left':
|
|
196
|
+
default:
|
|
197
|
+
return {
|
|
198
|
+
x: parentSymbol.x,
|
|
199
|
+
y: parentSymbol.y - distance
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolves a sheet-entry side code into a readable label.
|
|
206
|
+
* @param {number | null} side
|
|
207
|
+
* @returns {'left' | 'right' | 'top' | 'bottom'}
|
|
208
|
+
*/
|
|
209
|
+
static #resolveSheetEntrySide(side) {
|
|
210
|
+
switch (side) {
|
|
211
|
+
case 1:
|
|
212
|
+
return 'right'
|
|
213
|
+
case 2:
|
|
214
|
+
return 'top'
|
|
215
|
+
case 3:
|
|
216
|
+
return 'bottom'
|
|
217
|
+
case 0:
|
|
218
|
+
default:
|
|
219
|
+
return 'left'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolves an Altium I/O code into a readable direction label.
|
|
225
|
+
* @param {number | null} ioType
|
|
226
|
+
* @returns {'unspecified' | 'output' | 'input' | 'bidirectional'}
|
|
227
|
+
*/
|
|
228
|
+
static #resolveSheetEntryDirection(ioType) {
|
|
229
|
+
switch (ioType) {
|
|
230
|
+
case 1:
|
|
231
|
+
return 'output'
|
|
232
|
+
case 2:
|
|
233
|
+
return 'input'
|
|
234
|
+
case 3:
|
|
235
|
+
return 'bidirectional'
|
|
236
|
+
case 0:
|
|
237
|
+
default:
|
|
238
|
+
return 'unspecified'
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves standard schematic sheet-style display overrides.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicSheetStyleResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the displayed horizontal sheet-zone count after the page size
|
|
11
|
+
* has been normalized.
|
|
12
|
+
* @param {{ width: number, height: number, xZones: number, paperSize?: string, sheetStyle?: number }} sheet
|
|
13
|
+
* @returns {number}
|
|
14
|
+
*/
|
|
15
|
+
static resolveXZones(sheet) {
|
|
16
|
+
const configuredXZones = Math.max(Number(sheet?.xZones || 0), 1)
|
|
17
|
+
const paperSize = String(sheet?.paperSize || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.toUpperCase()
|
|
20
|
+
|
|
21
|
+
if (Number(sheet?.sheetStyle || 0) !== 1 && !paperSize) {
|
|
22
|
+
return configuredXZones
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const width = Number(sheet?.width || 0)
|
|
26
|
+
const height = Number(sheet?.height || 0)
|
|
27
|
+
if (width < height) {
|
|
28
|
+
return configuredXZones
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
paperSize === 'A2' ||
|
|
33
|
+
(width === 2339 && height === 1654) ||
|
|
34
|
+
paperSize === 'A3' ||
|
|
35
|
+
(width === 1654 && height === 1169)
|
|
36
|
+
) {
|
|
37
|
+
return 8
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (paperSize === 'A4' || (width === 1169 && height === 827)) {
|
|
41
|
+
return 4
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return configuredXZones
|
|
45
|
+
}
|
|
46
|
+
}
|