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,173 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
6
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
|
+
|
|
8
|
+
const { getField, parseBoolean, parseNumericField } = ParserUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes embedded and external schematic image records.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicImageParser {
|
|
14
|
+
/**
|
|
15
|
+
* Parses schematic image records and resolves embedded payloads when the
|
|
16
|
+
* file is an OLE container.
|
|
17
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
|
|
18
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
19
|
+
* @returns {{ images: { x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string }[], diagnostics: { severity: 'info' | 'warning', message: string }[] }}
|
|
20
|
+
*/
|
|
21
|
+
static parseSchematicImages(records, arrayBuffer) {
|
|
22
|
+
const diagnostics = []
|
|
23
|
+
const imageRecords = records.filter(
|
|
24
|
+
(record) => getField(record.fields, 'RECORD') === '30'
|
|
25
|
+
)
|
|
26
|
+
let oleDocument = null
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
imageRecords.some((record) =>
|
|
30
|
+
SchematicImageParser.#isEmbedded(record.fields)
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
try {
|
|
34
|
+
oleDocument = OleCompoundDocument.fromArrayBuffer(arrayBuffer)
|
|
35
|
+
} catch {
|
|
36
|
+
oleDocument = null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const images = imageRecords
|
|
41
|
+
.map((record) =>
|
|
42
|
+
SchematicImageParser.#parseSchematicImageRecord(
|
|
43
|
+
record,
|
|
44
|
+
oleDocument,
|
|
45
|
+
diagnostics
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
|
|
50
|
+
return { images, diagnostics }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns true when one record requests an embedded image payload.
|
|
55
|
+
* @param {Record<string, string | string[]>} fields
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
static #isEmbedded(fields) {
|
|
59
|
+
return parseBoolean(fields.EmbedImage || fields.EMBEDIMAGE)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes one image placement record.
|
|
64
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
|
|
65
|
+
* @param {OleCompoundDocument | null} oleDocument
|
|
66
|
+
* @param {{ severity: 'info' | 'warning', message: string }[]} diagnostics
|
|
67
|
+
* @returns {{ x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string } | null}
|
|
68
|
+
*/
|
|
69
|
+
static #parseSchematicImageRecord(record, oleDocument, diagnostics) {
|
|
70
|
+
const x = parseNumericField(record.fields, 'Location.X')
|
|
71
|
+
const y = parseNumericField(record.fields, 'Location.Y')
|
|
72
|
+
const cornerX = parseNumericField(record.fields, 'Corner.X')
|
|
73
|
+
const cornerY = parseNumericField(record.fields, 'Corner.Y')
|
|
74
|
+
|
|
75
|
+
if (x === null || y === null || cornerX === null || cornerY === null) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fileName =
|
|
80
|
+
getField(record.fields, 'FileName') ||
|
|
81
|
+
getField(record.fields, 'FILENAME')
|
|
82
|
+
const embedded = SchematicImageParser.#isEmbedded(record.fields)
|
|
83
|
+
const keepAspect = parseBoolean(
|
|
84
|
+
record.fields.KeepAspect || record.fields.KEEPASPECT
|
|
85
|
+
)
|
|
86
|
+
const renderOrder =
|
|
87
|
+
parseNumericField(record.fields, 'IndexInSheet') ??
|
|
88
|
+
record.recordIndex
|
|
89
|
+
let mimeType = ''
|
|
90
|
+
let dataBase64 = ''
|
|
91
|
+
let diagnosticState = embedded ? 'missing-embedded-payload' : 'external'
|
|
92
|
+
|
|
93
|
+
if (embedded && fileName && oleDocument) {
|
|
94
|
+
try {
|
|
95
|
+
const streamBytes = oleDocument.getStream(fileName)
|
|
96
|
+
mimeType = SchematicImageParser.#inferMimeType(fileName)
|
|
97
|
+
dataBase64 = SchematicImageParser.#encodeBase64(streamBytes)
|
|
98
|
+
diagnosticState = 'embedded'
|
|
99
|
+
} catch {
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
message:
|
|
103
|
+
'Embedded schematic image payload could not be resolved for ' +
|
|
104
|
+
fileName +
|
|
105
|
+
'.'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
} else if (embedded) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
severity: 'warning',
|
|
111
|
+
message:
|
|
112
|
+
'Embedded schematic image payload could not be resolved for ' +
|
|
113
|
+
(fileName || 'unnamed image') +
|
|
114
|
+
'.'
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
cornerX,
|
|
122
|
+
cornerY,
|
|
123
|
+
fileName,
|
|
124
|
+
embedded,
|
|
125
|
+
keepAspect,
|
|
126
|
+
mimeType,
|
|
127
|
+
dataBase64,
|
|
128
|
+
renderOrder,
|
|
129
|
+
diagnosticState
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Infers a MIME type from one file name.
|
|
135
|
+
* @param {string} fileName
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
static #inferMimeType(fileName) {
|
|
139
|
+
const normalized = String(fileName || '').toLowerCase()
|
|
140
|
+
|
|
141
|
+
if (normalized.endsWith('.bmp')) return 'image/bmp'
|
|
142
|
+
if (normalized.endsWith('.gif')) return 'image/gif'
|
|
143
|
+
if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg')) {
|
|
144
|
+
return 'image/jpeg'
|
|
145
|
+
}
|
|
146
|
+
if (normalized.endsWith('.png')) return 'image/png'
|
|
147
|
+
if (normalized.endsWith('.svg')) return 'image/svg+xml'
|
|
148
|
+
if (normalized.endsWith('.tif') || normalized.endsWith('.tiff')) {
|
|
149
|
+
return 'image/tiff'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Encodes one byte array as base64 in both browser and test environments.
|
|
157
|
+
* @param {Uint8Array} bytes
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
static #encodeBase64(bytes) {
|
|
161
|
+
if (typeof Buffer !== 'undefined') {
|
|
162
|
+
return Buffer.from(bytes).toString('base64')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let binary = ''
|
|
166
|
+
|
|
167
|
+
for (const byte of bytes) {
|
|
168
|
+
binary += String.fromCharCode(byte)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return btoa(binary)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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, parseNumericField, toColor } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes explicit schematic junction records.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicJunctionParser {
|
|
13
|
+
/**
|
|
14
|
+
* Parses authored schematic junction dots from `RECORD=29`.
|
|
15
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
|
|
16
|
+
* @returns {{ x: number, y: number, color: string, renderOrder: number }[]}
|
|
17
|
+
*/
|
|
18
|
+
static parseSchematicJunctions(records) {
|
|
19
|
+
return records
|
|
20
|
+
.map((record) => {
|
|
21
|
+
if (getField(record.fields, 'RECORD') !== '29') {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const x = parseNumericField(record.fields, 'Location.X')
|
|
26
|
+
const y = parseNumericField(record.fields, 'Location.Y')
|
|
27
|
+
|
|
28
|
+
if (x === null || y === null) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
x,
|
|
34
|
+
y,
|
|
35
|
+
color: toColor(record.fields.Color, '#000080'),
|
|
36
|
+
renderOrder:
|
|
37
|
+
parseNumericField(record.fields, 'IndexInSheet') ??
|
|
38
|
+
record.recordIndex
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
}
|
|
43
|
+
}
|