altium-toolkit 0.1.0 → 0.1.16
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/README.md +24 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +21 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -0,0 +1,355 @@
|
|
|
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 { getDisplayText, getField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves visible schematic component labels from owner-linked and nearby text.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicComponentTextResolver {
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a component designator from owner-linked text or nearby labels.
|
|
15
|
+
* @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
|
|
16
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
17
|
+
* @param {{ x: number, y: number, libReference: string }} component
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
static resolveDesignator(ownerTexts, texts, component) {
|
|
21
|
+
const ownerDesignator = SchematicComponentTextResolver.#findRelatedText(
|
|
22
|
+
ownerTexts,
|
|
23
|
+
'Designator'
|
|
24
|
+
)
|
|
25
|
+
if (
|
|
26
|
+
SchematicComponentTextResolver.#isResolvedComponentText(
|
|
27
|
+
ownerDesignator
|
|
28
|
+
)
|
|
29
|
+
) {
|
|
30
|
+
return ownerDesignator
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return SchematicComponentTextResolver.#findNearbyComponentDesignator(
|
|
34
|
+
texts,
|
|
35
|
+
component
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a component value from owner-linked text or nearby labels.
|
|
41
|
+
* @param {{ fields: Record<string, string | string[]> }[]} ownerTexts
|
|
42
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
43
|
+
* @param {{ x: number, y: number, libReference: string }} component
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
static resolveValue(ownerTexts, texts, component) {
|
|
47
|
+
const ownerValue =
|
|
48
|
+
SchematicComponentTextResolver.#findRelatedText(
|
|
49
|
+
ownerTexts,
|
|
50
|
+
'Comment'
|
|
51
|
+
) ||
|
|
52
|
+
SchematicComponentTextResolver.#findRelatedText(ownerTexts, 'VALUE')
|
|
53
|
+
if (
|
|
54
|
+
SchematicComponentTextResolver.#isResolvedComponentText(ownerValue)
|
|
55
|
+
) {
|
|
56
|
+
return ownerValue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
SchematicComponentTextResolver.#findNearbyComponentText(
|
|
61
|
+
texts,
|
|
62
|
+
component,
|
|
63
|
+
['comment', 'value'],
|
|
64
|
+
'',
|
|
65
|
+
SchematicComponentTextResolver.#inferComponentValueHint(
|
|
66
|
+
component.libReference
|
|
67
|
+
)
|
|
68
|
+
) || ownerValue
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Finds a related text value by name.
|
|
74
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
75
|
+
* @param {string} logicalName
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
static #findRelatedText(records, logicalName) {
|
|
79
|
+
const match = records.find(
|
|
80
|
+
(record) =>
|
|
81
|
+
getField(record.fields, 'Name').toLowerCase() ===
|
|
82
|
+
logicalName.toLowerCase()
|
|
83
|
+
)
|
|
84
|
+
return match ? getDisplayText(match.fields) : ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Finds the closest nearby designator text for one component.
|
|
89
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
90
|
+
* @param {{ x: number, y: number, libReference: string }} component
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
static #findNearbyComponentDesignator(texts, component) {
|
|
94
|
+
const expectedPrefix =
|
|
95
|
+
SchematicComponentTextResolver.#inferComponentDesignatorPrefix(
|
|
96
|
+
component.libReference
|
|
97
|
+
)
|
|
98
|
+
const expectedValueHint =
|
|
99
|
+
SchematicComponentTextResolver.#inferComponentValueHint(
|
|
100
|
+
component.libReference
|
|
101
|
+
)
|
|
102
|
+
const candidates =
|
|
103
|
+
SchematicComponentTextResolver.#collectNearbyComponentTextCandidates(
|
|
104
|
+
texts,
|
|
105
|
+
component,
|
|
106
|
+
['designator']
|
|
107
|
+
)
|
|
108
|
+
const scopedCandidates = expectedPrefix
|
|
109
|
+
? candidates.filter((candidate) =>
|
|
110
|
+
candidate.text
|
|
111
|
+
.toUpperCase()
|
|
112
|
+
.startsWith(expectedPrefix.toUpperCase())
|
|
113
|
+
)
|
|
114
|
+
: candidates
|
|
115
|
+
const usableCandidates = scopedCandidates.length
|
|
116
|
+
? scopedCandidates
|
|
117
|
+
: candidates
|
|
118
|
+
const rankedCandidates = usableCandidates
|
|
119
|
+
.map((candidate) => ({
|
|
120
|
+
...candidate,
|
|
121
|
+
score:
|
|
122
|
+
candidate.distance +
|
|
123
|
+
SchematicComponentTextResolver.#scoreAssociatedValueMismatch(
|
|
124
|
+
texts,
|
|
125
|
+
candidate,
|
|
126
|
+
expectedValueHint
|
|
127
|
+
)
|
|
128
|
+
}))
|
|
129
|
+
.sort((left, right) => left.score - right.score)
|
|
130
|
+
|
|
131
|
+
return rankedCandidates[0]?.text || ''
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Finds the closest nearby visible text for one component.
|
|
136
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
137
|
+
* @param {{ x: number, y: number }} component
|
|
138
|
+
* @param {string[]} logicalNames
|
|
139
|
+
* @param {string} expectedPrefix
|
|
140
|
+
* @param {string} expectedTextHint
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
static #findNearbyComponentText(
|
|
144
|
+
texts,
|
|
145
|
+
component,
|
|
146
|
+
logicalNames,
|
|
147
|
+
expectedPrefix = '',
|
|
148
|
+
expectedTextHint = ''
|
|
149
|
+
) {
|
|
150
|
+
const candidates =
|
|
151
|
+
SchematicComponentTextResolver.#collectNearbyComponentTextCandidates(
|
|
152
|
+
texts,
|
|
153
|
+
component,
|
|
154
|
+
logicalNames
|
|
155
|
+
)
|
|
156
|
+
const prefixedCandidates = expectedPrefix
|
|
157
|
+
? candidates.filter((candidate) =>
|
|
158
|
+
candidate.text
|
|
159
|
+
.toUpperCase()
|
|
160
|
+
.startsWith(expectedPrefix.toUpperCase())
|
|
161
|
+
)
|
|
162
|
+
: candidates
|
|
163
|
+
const scopedCandidates = prefixedCandidates.length
|
|
164
|
+
? prefixedCandidates
|
|
165
|
+
: candidates
|
|
166
|
+
const hintedCandidates = expectedTextHint
|
|
167
|
+
? scopedCandidates.filter((candidate) =>
|
|
168
|
+
SchematicComponentTextResolver.#normalizeTextMatch(
|
|
169
|
+
candidate.text
|
|
170
|
+
).includes(
|
|
171
|
+
SchematicComponentTextResolver.#normalizeTextMatch(
|
|
172
|
+
expectedTextHint
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
: scopedCandidates
|
|
177
|
+
const usableCandidates = hintedCandidates.length
|
|
178
|
+
? hintedCandidates
|
|
179
|
+
: scopedCandidates
|
|
180
|
+
|
|
181
|
+
return usableCandidates.sort(
|
|
182
|
+
(left, right) => left.distance - right.distance
|
|
183
|
+
)[0]?.text
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Collects nearby visible schematic text candidates around one component.
|
|
188
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
189
|
+
* @param {{ x: number, y: number }} component
|
|
190
|
+
* @param {string[]} logicalNames
|
|
191
|
+
* @returns {{ x: number, y: number, text: string, distance: number }[]}
|
|
192
|
+
*/
|
|
193
|
+
static #collectNearbyComponentTextCandidates(
|
|
194
|
+
texts,
|
|
195
|
+
component,
|
|
196
|
+
logicalNames
|
|
197
|
+
) {
|
|
198
|
+
const allowedNames = new Set(
|
|
199
|
+
logicalNames.map((name) => name.toLowerCase())
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return texts
|
|
203
|
+
.filter((text) =>
|
|
204
|
+
allowedNames.has(
|
|
205
|
+
String(text.name || '')
|
|
206
|
+
.trim()
|
|
207
|
+
.toLowerCase()
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
.map((text) => ({
|
|
211
|
+
x: text.x,
|
|
212
|
+
y: text.y,
|
|
213
|
+
text: text.text,
|
|
214
|
+
distance:
|
|
215
|
+
Math.abs(text.x - component.x) +
|
|
216
|
+
Math.abs(text.y - component.y)
|
|
217
|
+
}))
|
|
218
|
+
.filter(
|
|
219
|
+
(text) =>
|
|
220
|
+
Math.abs(text.x - component.x) <= 80 &&
|
|
221
|
+
Math.abs(text.y - component.y) <= 80
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Penalizes a designator candidate when nearby value text mismatches.
|
|
227
|
+
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
228
|
+
* @param {{ x: number, y: number }} candidate
|
|
229
|
+
* @param {string} expectedValueHint
|
|
230
|
+
* @returns {number}
|
|
231
|
+
*/
|
|
232
|
+
static #scoreAssociatedValueMismatch(texts, candidate, expectedValueHint) {
|
|
233
|
+
if (!expectedValueHint) {
|
|
234
|
+
return 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const associatedValue =
|
|
238
|
+
SchematicComponentTextResolver.#findNearbyComponentText(
|
|
239
|
+
texts,
|
|
240
|
+
candidate,
|
|
241
|
+
['comment', 'value']
|
|
242
|
+
)
|
|
243
|
+
if (!associatedValue) {
|
|
244
|
+
return 0
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return SchematicComponentTextResolver.#normalizeTextMatch(
|
|
248
|
+
associatedValue
|
|
249
|
+
).includes(
|
|
250
|
+
SchematicComponentTextResolver.#normalizeTextMatch(
|
|
251
|
+
expectedValueHint
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
? -30
|
|
255
|
+
: 30
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns true when owner-linked text is usable as a display value.
|
|
260
|
+
* @param {string} value
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*/
|
|
263
|
+
static #isResolvedComponentText(value) {
|
|
264
|
+
const normalized = String(value || '').trim()
|
|
265
|
+
|
|
266
|
+
return Boolean(
|
|
267
|
+
normalized && normalized !== '*' && !normalized.startsWith('=')
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Infers the visible designator prefix from a library reference.
|
|
273
|
+
* @param {string} libReference
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
static #inferComponentDesignatorPrefix(libReference) {
|
|
277
|
+
const normalized = String(libReference || '')
|
|
278
|
+
.trim()
|
|
279
|
+
.toUpperCase()
|
|
280
|
+
|
|
281
|
+
if (normalized.startsWith('RES/')) return 'R'
|
|
282
|
+
if (normalized.startsWith('CAP/')) return 'C'
|
|
283
|
+
if (normalized.startsWith('DIODE/')) return 'D'
|
|
284
|
+
if (normalized.startsWith('CON/')) return 'J'
|
|
285
|
+
if (normalized.startsWith('IC/')) return 'U'
|
|
286
|
+
|
|
287
|
+
return ''
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Infers the visible value label from a library reference.
|
|
292
|
+
* @param {string} libReference
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
static #inferComponentValueHint(libReference) {
|
|
296
|
+
const segments = String(libReference || '')
|
|
297
|
+
.split('/')
|
|
298
|
+
.map((segment) => segment.trim())
|
|
299
|
+
.filter(Boolean)
|
|
300
|
+
|
|
301
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
302
|
+
const segment = segments[index]
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
SchematicComponentTextResolver.#isPackageLikeComponentSegment(
|
|
306
|
+
segment
|
|
307
|
+
) ||
|
|
308
|
+
/\s/.test(segment)
|
|
309
|
+
) {
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
/^(?:\d+(?:\.\d+)?(?:R|K|M|UF|NF|PF)|1N[A-Z0-9-]+)$/i.test(
|
|
315
|
+
segment
|
|
316
|
+
)
|
|
317
|
+
) {
|
|
318
|
+
return segment
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
/[A-Z]/i.test(segment) &&
|
|
323
|
+
/\d/.test(segment) &&
|
|
324
|
+
segment.length >= 6
|
|
325
|
+
) {
|
|
326
|
+
return segment
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return ''
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Returns true when a library segment is package/rating-like.
|
|
335
|
+
* @param {string} segment
|
|
336
|
+
* @returns {boolean}
|
|
337
|
+
*/
|
|
338
|
+
static #isPackageLikeComponentSegment(segment) {
|
|
339
|
+
return /^(?:CE|\d{4}|SC\d+|SOD-\d+|\d+(?:\.\d+)?V|\d+(?:\.\d+)?[%%])$/i.test(
|
|
340
|
+
String(segment || '').trim()
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Normalizes a text fragment for proximity matching.
|
|
346
|
+
* @param {string} value
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
static #normalizeTextMatch(value) {
|
|
350
|
+
return String(value || '')
|
|
351
|
+
.toUpperCase()
|
|
352
|
+
.replaceAll(/\s+/g, '')
|
|
353
|
+
.replaceAll('%', '%')
|
|
354
|
+
}
|
|
355
|
+
}
|
package/src/parser.mjs
CHANGED
|
@@ -10,12 +10,25 @@ export { AltiumParser } from './core/altium/AltiumParser.mjs'
|
|
|
10
10
|
export { AltiumLayoutParser } from './core/altium/AltiumLayoutParser.mjs'
|
|
11
11
|
export { AsciiRecordParser } from './core/altium/AsciiRecordParser.mjs'
|
|
12
12
|
export { ParserUtils } from './core/altium/ParserUtils.mjs'
|
|
13
|
+
export { NormalizedModelSchema } from './core/altium/NormalizedModelSchema.mjs'
|
|
13
14
|
export { PcbBinaryPrimitiveParser } from './core/altium/PcbBinaryPrimitiveParser.mjs'
|
|
15
|
+
export { PcbBoardRegionSemanticsParser } from './core/altium/PcbBoardRegionSemanticsParser.mjs'
|
|
16
|
+
export { PcbComponentPrimitiveIndexer } from './core/altium/PcbComponentPrimitiveIndexer.mjs'
|
|
17
|
+
export { PcbEmbeddedFontExtractor } from './core/altium/PcbEmbeddedFontExtractor.mjs'
|
|
14
18
|
export { PcbEmbeddedModelExtractor } from './core/altium/PcbEmbeddedModelExtractor.mjs'
|
|
19
|
+
export { PcbFontMetricsParser } from './core/altium/PcbFontMetricsParser.mjs'
|
|
20
|
+
export { PcbLayerIdCodec } from './core/altium/PcbLayerIdCodec.mjs'
|
|
21
|
+
export { PcbLibModelParser } from './core/altium/PcbLibModelParser.mjs'
|
|
22
|
+
export { PcbLibStreamExtractor } from './core/altium/PcbLibStreamExtractor.mjs'
|
|
15
23
|
export { PcbModelParser } from './core/altium/PcbModelParser.mjs'
|
|
16
24
|
export { PcbOutlineRasterizer } from './core/altium/PcbOutlineRasterizer.mjs'
|
|
17
25
|
export { PcbOutlineRecovery } from './core/altium/PcbOutlineRecovery.mjs'
|
|
26
|
+
export { PcbPadStackParser } from './core/altium/PcbPadStackParser.mjs'
|
|
27
|
+
export { PcbRawRecordRegistry } from './core/altium/PcbRawRecordRegistry.mjs'
|
|
28
|
+
export { PcbRuleParser } from './core/altium/PcbRuleParser.mjs'
|
|
18
29
|
export { PcbStreamExtractor } from './core/altium/PcbStreamExtractor.mjs'
|
|
30
|
+
export { PcbViaStackParser } from './core/altium/PcbViaStackParser.mjs'
|
|
19
31
|
export { PrintableTextDecoder } from './core/altium/PrintableTextDecoder.mjs'
|
|
32
|
+
export { PrjPcbModelParser } from './core/altium/PrjPcbModelParser.mjs'
|
|
20
33
|
export { SchematicMultipartOwnerMatcher } from './core/altium/SchematicMultipartOwnerMatcher.mjs'
|
|
21
34
|
export { SchematicStandaloneCalloutNormalizer } from './core/altium/SchematicStandaloneCalloutNormalizer.mjs'
|
package/src/renderers.mjs
CHANGED
|
@@ -6,6 +6,11 @@ export { BomTableRenderer } from './ui/BomTableRenderer.mjs'
|
|
|
6
6
|
export { PcbArcUtils } from './ui/PcbArcUtils.mjs'
|
|
7
7
|
export { PcbEdgeFacingGlyphNormalizer } from './ui/PcbEdgeFacingGlyphNormalizer.mjs'
|
|
8
8
|
export { PcbFootprintPrimitiveSelector } from './ui/PcbFootprintPrimitiveSelector.mjs'
|
|
9
|
+
export {
|
|
10
|
+
PcbSideResolvedRenderModel,
|
|
11
|
+
isCopperPrimitive,
|
|
12
|
+
preparePcbSideResolvedRenderModel
|
|
13
|
+
} from './ui/PcbSideResolvedRenderModel.mjs'
|
|
9
14
|
export { PcbSvgRenderer } from './ui/PcbSvgRenderer.mjs'
|
|
10
15
|
export { SchematicColorResolver } from './ui/SchematicColorResolver.mjs'
|
|
11
16
|
export { SchematicContentLayout } from './ui/SchematicContentLayout.mjs'
|
|
@@ -186,7 +186,8 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
.pcb-polygon,
|
|
189
|
-
.pcb-fill
|
|
189
|
+
.pcb-fill,
|
|
190
|
+
.pcb-region {
|
|
190
191
|
stroke: none;
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -207,11 +208,13 @@
|
|
|
207
208
|
fill: var(--pcb-subsurface-copper-fill);
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
.pcb-copper--surface .pcb-fill
|
|
211
|
+
.pcb-copper--surface .pcb-fill,
|
|
212
|
+
.pcb-copper--surface .pcb-region {
|
|
211
213
|
fill: var(--pcb-surface-fill);
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
.pcb-copper--subsurface .pcb-fill
|
|
216
|
+
.pcb-copper--subsurface .pcb-fill,
|
|
217
|
+
.pcb-copper--subsurface .pcb-region {
|
|
215
218
|
fill: var(--pcb-subsurface-fill);
|
|
216
219
|
}
|
|
217
220
|
|
|
@@ -244,7 +247,8 @@
|
|
|
244
247
|
fill: var(--pcb-copper-solid-fill);
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
.pcb-footprint-fill
|
|
250
|
+
.pcb-footprint-fill,
|
|
251
|
+
.pcb-footprint-region {
|
|
248
252
|
fill: var(--pcb-footprint-fill);
|
|
249
253
|
}
|
|
250
254
|
|
|
@@ -263,11 +267,12 @@
|
|
|
263
267
|
fill: var(--pcb-component-bottom-fill);
|
|
264
268
|
}
|
|
265
269
|
|
|
266
|
-
.pcb-
|
|
270
|
+
.pcb-text {
|
|
267
271
|
font-size: 29px;
|
|
268
|
-
text-anchor: middle;
|
|
269
272
|
fill: var(--pcb-component-text);
|
|
270
273
|
font-weight: 700;
|
|
274
|
+
font-family: Arial, sans-serif;
|
|
275
|
+
pointer-events: none;
|
|
271
276
|
}
|
|
272
277
|
|
|
273
278
|
.bom-panel {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits PCB copper primitives into surface and subsurface render groups.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbCopperPrimitiveSplitter {
|
|
9
|
+
/**
|
|
10
|
+
* Splits recovered copper primitives into the default top-facing surface
|
|
11
|
+
* view and de-emphasized buried layers.
|
|
12
|
+
* @param {{ layer?: string, segments: Array<Record<string, number | string>> }[]} polygons
|
|
13
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[]} fills
|
|
14
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[]} tracks
|
|
15
|
+
* @param {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[]} arcs
|
|
16
|
+
* @param {{ points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[]} regions
|
|
17
|
+
* @returns {{ surface: { polygons: { layer?: string, segments: Array<Record<string, number | string>> }[], fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] }, subsurface: { polygons: { layer?: string, segments: Array<Record<string, number | string>> }[], fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] } }}
|
|
18
|
+
*/
|
|
19
|
+
static split(polygons, fills, tracks, arcs, regions) {
|
|
20
|
+
const copperFills = fills.filter((fill) =>
|
|
21
|
+
PcbCopperPrimitiveSplitter.#isCopperLayerId(fill.layerId)
|
|
22
|
+
)
|
|
23
|
+
const copperTracks = tracks.filter((track) =>
|
|
24
|
+
PcbCopperPrimitiveSplitter.#isCopperLayerId(track.layerId)
|
|
25
|
+
)
|
|
26
|
+
const copperArcs = arcs.filter((arc) =>
|
|
27
|
+
PcbCopperPrimitiveSplitter.#isCopperLayerId(arc.layerId)
|
|
28
|
+
)
|
|
29
|
+
const copperRegions = regions.filter((region) =>
|
|
30
|
+
PcbCopperPrimitiveSplitter.#isCopperLayerId(region.layerId)
|
|
31
|
+
)
|
|
32
|
+
const surfaceTrackLayerCode =
|
|
33
|
+
PcbCopperPrimitiveSplitter.#resolveSurfaceLayerCode(copperTracks)
|
|
34
|
+
const surfaceFillLayerCode =
|
|
35
|
+
PcbCopperPrimitiveSplitter.#resolveSurfaceLayerCode(copperFills)
|
|
36
|
+
const surfaceArcLayerCode =
|
|
37
|
+
PcbCopperPrimitiveSplitter.#resolveSurfaceLayerCode(copperArcs)
|
|
38
|
+
const surfaceRegionLayerCode =
|
|
39
|
+
PcbCopperPrimitiveSplitter.#resolveSurfaceLayerCode(copperRegions)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
surface: {
|
|
43
|
+
polygons: polygons.filter((polygon) =>
|
|
44
|
+
PcbCopperPrimitiveSplitter.#isSurfacePolygon(polygon)
|
|
45
|
+
),
|
|
46
|
+
fills: copperFills.filter(
|
|
47
|
+
(fill) => fill.layerCode === surfaceFillLayerCode
|
|
48
|
+
),
|
|
49
|
+
tracks: copperTracks.filter(
|
|
50
|
+
(track) => track.layerCode === surfaceTrackLayerCode
|
|
51
|
+
),
|
|
52
|
+
arcs: copperArcs.filter(
|
|
53
|
+
(arc) => arc.layerCode === surfaceArcLayerCode
|
|
54
|
+
),
|
|
55
|
+
regions: copperRegions.filter(
|
|
56
|
+
(region) => region.layerCode === surfaceRegionLayerCode
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
subsurface: {
|
|
60
|
+
polygons: polygons.filter(
|
|
61
|
+
(polygon) =>
|
|
62
|
+
!PcbCopperPrimitiveSplitter.#isSurfacePolygon(polygon)
|
|
63
|
+
),
|
|
64
|
+
fills: copperFills.filter(
|
|
65
|
+
(fill) => fill.layerCode !== surfaceFillLayerCode
|
|
66
|
+
),
|
|
67
|
+
tracks: copperTracks.filter(
|
|
68
|
+
(track) => track.layerCode !== surfaceTrackLayerCode
|
|
69
|
+
),
|
|
70
|
+
arcs: copperArcs.filter(
|
|
71
|
+
(arc) => arc.layerCode !== surfaceArcLayerCode
|
|
72
|
+
),
|
|
73
|
+
regions: copperRegions.filter(
|
|
74
|
+
(region) => region.layerCode !== surfaceRegionLayerCode
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the default visible layer code from one primitive family.
|
|
82
|
+
* @param {{ layerCode?: number }[]} primitives
|
|
83
|
+
* @returns {number | null}
|
|
84
|
+
*/
|
|
85
|
+
static #resolveSurfaceLayerCode(primitives) {
|
|
86
|
+
const layerCodes = primitives
|
|
87
|
+
.map((primitive) => primitive.layerCode)
|
|
88
|
+
.filter((layerCode) => Number.isFinite(layerCode))
|
|
89
|
+
return layerCodes.length ? Math.min(...layerCodes) : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns true when one polygon belongs to the top-facing copper view.
|
|
94
|
+
* @param {{ layer?: string }} polygon
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
static #isSurfacePolygon(polygon) {
|
|
98
|
+
return (
|
|
99
|
+
String(polygon.layer || '')
|
|
100
|
+
.trim()
|
|
101
|
+
.toUpperCase() === 'TOP'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns true when one decoded primitive layer belongs to copper.
|
|
107
|
+
* @param {number | undefined} layerId
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
static #isCopperLayerId(layerId) {
|
|
111
|
+
return Number.isInteger(layerId) && layerId >= 1 && layerId <= 32
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -19,10 +19,10 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
19
19
|
* Normalizes repeated edge-facing documentation glyphs so their opening
|
|
20
20
|
* stays on the board edge even when the authored primitive cluster is
|
|
21
21
|
* mirrored inward.
|
|
22
|
-
* @param {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[] }} footprintPrimitives
|
|
22
|
+
* @param {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions?: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] }} footprintPrimitives
|
|
23
23
|
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} outline
|
|
24
24
|
* @param {{ preferMarkers?: boolean }} [options]
|
|
25
|
-
* @returns {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[] }}
|
|
25
|
+
* @returns {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] }}
|
|
26
26
|
*/
|
|
27
27
|
static normalize(footprintPrimitives, outline, options = {}) {
|
|
28
28
|
const normalizedTracks = (footprintPrimitives?.tracks || []).map(
|
|
@@ -61,16 +61,17 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
61
61
|
return {
|
|
62
62
|
fills: footprintPrimitives?.fills || [],
|
|
63
63
|
tracks: normalizedTracks,
|
|
64
|
-
arcs: normalizedArcs
|
|
64
|
+
arcs: normalizedArcs,
|
|
65
|
+
regions: footprintPrimitives?.regions || []
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Normalizes glyphs using only the nearest board edge so 3D silkscreen
|
|
70
71
|
* detail does not overreact to nearby circular markers on other features.
|
|
71
|
-
* @param {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[] }} footprintPrimitives
|
|
72
|
+
* @param {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions?: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] }} footprintPrimitives
|
|
72
73
|
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} outline
|
|
73
|
-
* @returns {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[] }}
|
|
74
|
+
* @returns {{ fills: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], regions: { points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[] }}
|
|
74
75
|
*/
|
|
75
76
|
static normalizeForBoardEdge(footprintPrimitives, outline) {
|
|
76
77
|
return PcbEdgeFacingGlyphNormalizer.normalize(
|