altium-toolkit 0.1.1 → 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 +6 -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,336 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Prepares normalized Altium PCB models for side-specific top-oriented renderers.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbSideResolvedRenderModel {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a normalized PCB model for the requested board side.
|
|
11
|
+
* @param {object | null} board
|
|
12
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back' }} [options]
|
|
13
|
+
* @returns {object | null}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(board, options = {}) {
|
|
16
|
+
if (!board?.pcb) return board || null
|
|
17
|
+
|
|
18
|
+
const side = PcbSideResolvedRenderModel.#normalizeSide(options)
|
|
19
|
+
const pcb = board.pcb
|
|
20
|
+
return {
|
|
21
|
+
...board,
|
|
22
|
+
pcb: {
|
|
23
|
+
...pcb,
|
|
24
|
+
components: (pcb.components || []).filter((component) =>
|
|
25
|
+
PcbSideResolvedRenderModel.#isComponentVisibleOnSide(
|
|
26
|
+
component,
|
|
27
|
+
side
|
|
28
|
+
)
|
|
29
|
+
),
|
|
30
|
+
primitiveLayers:
|
|
31
|
+
PcbSideResolvedRenderModel.#preparePrimitiveLayersForSide(
|
|
32
|
+
pcb.primitiveLayers || [],
|
|
33
|
+
side
|
|
34
|
+
),
|
|
35
|
+
polygons: PcbSideResolvedRenderModel.#preparePolygonsForSide(
|
|
36
|
+
pcb.polygons || [],
|
|
37
|
+
side
|
|
38
|
+
),
|
|
39
|
+
fills: PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
40
|
+
pcb.fills || [],
|
|
41
|
+
side
|
|
42
|
+
),
|
|
43
|
+
tracks: PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
44
|
+
pcb.tracks || [],
|
|
45
|
+
side
|
|
46
|
+
),
|
|
47
|
+
arcs: PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
48
|
+
pcb.arcs || [],
|
|
49
|
+
side
|
|
50
|
+
),
|
|
51
|
+
regions: PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
52
|
+
pcb.regions || [],
|
|
53
|
+
side
|
|
54
|
+
),
|
|
55
|
+
shapeBasedRegions:
|
|
56
|
+
PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
57
|
+
pcb.shapeBasedRegions || [],
|
|
58
|
+
side
|
|
59
|
+
),
|
|
60
|
+
boardRegions:
|
|
61
|
+
PcbSideResolvedRenderModel.#preparePrimitivesForSide(
|
|
62
|
+
pcb.boardRegions || [],
|
|
63
|
+
side
|
|
64
|
+
),
|
|
65
|
+
vias: PcbSideResolvedRenderModel.#prepareViasForSide(
|
|
66
|
+
pcb.vias || [],
|
|
67
|
+
side
|
|
68
|
+
),
|
|
69
|
+
pads: PcbSideResolvedRenderModel.#preparePadsForSide(
|
|
70
|
+
pcb.pads || [],
|
|
71
|
+
side
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks whether a primitive belongs to an Altium copper signal layer.
|
|
79
|
+
* @param {object | null} primitive
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
static isCopperPrimitive(primitive) {
|
|
83
|
+
const layerId = Number(primitive?.layerId)
|
|
84
|
+
return Number.isInteger(layerId) && layerId >= 1 && layerId <= 32
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalizes the caller side option.
|
|
89
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back' }} options
|
|
90
|
+
* @returns {'front' | 'back'}
|
|
91
|
+
*/
|
|
92
|
+
static #normalizeSide(options) {
|
|
93
|
+
if (options === 'back') return 'back'
|
|
94
|
+
if (options && typeof options === 'object' && options.side === 'back') {
|
|
95
|
+
return 'back'
|
|
96
|
+
}
|
|
97
|
+
return 'front'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Checks whether a component should be visible for a requested side.
|
|
102
|
+
* @param {object | null} component
|
|
103
|
+
* @param {'front' | 'back'} side
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
static #isComponentVisibleOnSide(component, side) {
|
|
107
|
+
const layer = String(component?.layer || '')
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
const isBottom = layer.includes('bottom') || layer === 'bot'
|
|
111
|
+
return side === 'back' ? isBottom : !isBottom
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Projects primitive layer names for a top-oriented renderer.
|
|
116
|
+
* @param {readonly object[]} primitiveLayers
|
|
117
|
+
* @param {'front' | 'back'} side
|
|
118
|
+
* @returns {object[]}
|
|
119
|
+
*/
|
|
120
|
+
static #preparePrimitiveLayersForSide(primitiveLayers, side) {
|
|
121
|
+
if (side !== 'back') return Array.from(primitiveLayers)
|
|
122
|
+
|
|
123
|
+
return primitiveLayers.map((layer) => ({
|
|
124
|
+
...layer,
|
|
125
|
+
name: PcbSideResolvedRenderModel.#backLayerNameForTopRenderer(
|
|
126
|
+
layer.name
|
|
127
|
+
)
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Converts top/bottom layer labels for back-side top-oriented rendering.
|
|
133
|
+
* @param {unknown} name
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
static #backLayerNameForTopRenderer(name) {
|
|
137
|
+
const value = String(name || '')
|
|
138
|
+
if (/\bbottom\b/iu.test(value)) {
|
|
139
|
+
return value.replace(/\bbottom\b/giu, 'Top')
|
|
140
|
+
}
|
|
141
|
+
if (/\btop\b/iu.test(value)) {
|
|
142
|
+
return value.replace(/\btop\b/giu, 'Hidden')
|
|
143
|
+
}
|
|
144
|
+
return value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Projects polygon side labels for a top-oriented renderer.
|
|
149
|
+
* @param {readonly object[]} polygons
|
|
150
|
+
* @param {'front' | 'back'} side
|
|
151
|
+
* @returns {object[]}
|
|
152
|
+
*/
|
|
153
|
+
static #preparePolygonsForSide(polygons, side) {
|
|
154
|
+
if (side !== 'back') return Array.from(polygons)
|
|
155
|
+
|
|
156
|
+
return polygons.map((polygon) => {
|
|
157
|
+
const layer = String(polygon?.layer || '')
|
|
158
|
+
.trim()
|
|
159
|
+
.toUpperCase()
|
|
160
|
+
if (layer === 'BOTTOM' || layer === 'BOT') {
|
|
161
|
+
return { ...polygon, layer: 'TOP' }
|
|
162
|
+
}
|
|
163
|
+
if (layer === 'TOP') {
|
|
164
|
+
return { ...polygon, layer: 'HIDDEN' }
|
|
165
|
+
}
|
|
166
|
+
return { ...polygon }
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Projects primitive layer codes for a top-oriented renderer.
|
|
172
|
+
* @param {readonly object[]} primitives
|
|
173
|
+
* @param {'front' | 'back'} side
|
|
174
|
+
* @returns {object[]}
|
|
175
|
+
*/
|
|
176
|
+
static #preparePrimitivesForSide(primitives, side) {
|
|
177
|
+
if (side !== 'back') return Array.from(primitives)
|
|
178
|
+
|
|
179
|
+
return primitives.map((primitive) => {
|
|
180
|
+
if (!PcbSideResolvedRenderModel.isCopperPrimitive(primitive)) {
|
|
181
|
+
return { ...primitive }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const layerCode = Number(primitive.layerCode)
|
|
185
|
+
const fallbackCode = Number(primitive.layerId)
|
|
186
|
+
return {
|
|
187
|
+
...primitive,
|
|
188
|
+
layerCode: -(Number.isFinite(layerCode)
|
|
189
|
+
? layerCode
|
|
190
|
+
: fallbackCode || 0)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Filters vias to those visible from the requested side.
|
|
197
|
+
* @param {readonly object[]} vias
|
|
198
|
+
* @param {'front' | 'back'} side
|
|
199
|
+
* @returns {object[]}
|
|
200
|
+
*/
|
|
201
|
+
static #prepareViasForSide(vias, side) {
|
|
202
|
+
return vias
|
|
203
|
+
.filter((via) =>
|
|
204
|
+
PcbSideResolvedRenderModel.#isViaVisibleOnSide(via, side)
|
|
205
|
+
)
|
|
206
|
+
.map((via) => ({ ...via }))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks whether a via spans the requested surface layer.
|
|
211
|
+
* @param {object | null} via
|
|
212
|
+
* @param {'front' | 'back'} side
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
static #isViaVisibleOnSide(via, side) {
|
|
216
|
+
const start = Number(via?.layerStartId)
|
|
217
|
+
const end = Number(via?.layerEndId)
|
|
218
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) return true
|
|
219
|
+
|
|
220
|
+
const surfaceLayerId = side === 'back' ? 32 : 1
|
|
221
|
+
return (
|
|
222
|
+
surfaceLayerId >= Math.min(start, end) &&
|
|
223
|
+
surfaceLayerId <= Math.max(start, end)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Filters and projects pads to the requested side.
|
|
229
|
+
* @param {readonly object[]} pads
|
|
230
|
+
* @param {'front' | 'back'} side
|
|
231
|
+
* @returns {object[]}
|
|
232
|
+
*/
|
|
233
|
+
static #preparePadsForSide(pads, side) {
|
|
234
|
+
const visiblePads = pads.filter((pad) =>
|
|
235
|
+
PcbSideResolvedRenderModel.#isPadVisibleOnSide(pad, side)
|
|
236
|
+
)
|
|
237
|
+
if (side !== 'back') return visiblePads.map((pad) => ({ ...pad }))
|
|
238
|
+
|
|
239
|
+
return visiblePads.map((pad) => {
|
|
240
|
+
const layerId = PcbSideResolvedRenderModel.#effectivePadLayerId(pad)
|
|
241
|
+
return {
|
|
242
|
+
...pad,
|
|
243
|
+
sizeTopX: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
244
|
+
pad.sizeBottomX,
|
|
245
|
+
pad.sizeMidX,
|
|
246
|
+
pad.sizeTopX
|
|
247
|
+
),
|
|
248
|
+
sizeTopY: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
249
|
+
pad.sizeBottomY,
|
|
250
|
+
pad.sizeMidY,
|
|
251
|
+
pad.sizeTopY
|
|
252
|
+
),
|
|
253
|
+
shapeTop: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
254
|
+
pad.shapeBottom,
|
|
255
|
+
pad.shapeMid,
|
|
256
|
+
pad.shapeTop
|
|
257
|
+
),
|
|
258
|
+
roundedRectShapeTop:
|
|
259
|
+
PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
260
|
+
pad.roundedRectShapeBottom,
|
|
261
|
+
pad.roundedRectShapeMid,
|
|
262
|
+
pad.roundedRectShapeTop
|
|
263
|
+
),
|
|
264
|
+
cornerRadiusTop: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
265
|
+
pad.cornerRadiusBottom,
|
|
266
|
+
pad.cornerRadiusMid,
|
|
267
|
+
pad.cornerRadiusTop
|
|
268
|
+
),
|
|
269
|
+
layerCode: layerId === 32 ? 1 : pad.layerCode,
|
|
270
|
+
layerId: layerId === 32 ? 1 : pad.layerId
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Checks whether a pad should be visible on the requested side.
|
|
277
|
+
* @param {object | null} pad
|
|
278
|
+
* @param {'front' | 'back'} side
|
|
279
|
+
* @returns {boolean}
|
|
280
|
+
*/
|
|
281
|
+
static #isPadVisibleOnSide(pad, side) {
|
|
282
|
+
const layerId = PcbSideResolvedRenderModel.#effectivePadLayerId(pad)
|
|
283
|
+
if (layerId === 1) return side === 'front'
|
|
284
|
+
if (layerId === 32) return side === 'back'
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the authored Altium layer id for a pad.
|
|
290
|
+
* @param {object | null} pad
|
|
291
|
+
* @returns {number | null}
|
|
292
|
+
*/
|
|
293
|
+
static #effectivePadLayerId(pad) {
|
|
294
|
+
const layerId = Number(pad?.layerId)
|
|
295
|
+
if (Number.isInteger(layerId) && layerId > 0) return layerId
|
|
296
|
+
|
|
297
|
+
const legacyLayerId = Number(pad?.legacyLayerId)
|
|
298
|
+
if (Number.isInteger(legacyLayerId) && legacyLayerId > 0) {
|
|
299
|
+
return legacyLayerId
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Returns the first finite numeric value.
|
|
307
|
+
* @param {...unknown} values
|
|
308
|
+
* @returns {number | undefined}
|
|
309
|
+
*/
|
|
310
|
+
static #firstFiniteValue(...values) {
|
|
311
|
+
for (const value of values) {
|
|
312
|
+
const number = Number(value)
|
|
313
|
+
if (Number.isFinite(number)) return number
|
|
314
|
+
}
|
|
315
|
+
return undefined
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolves a normalized PCB model for the requested board side.
|
|
321
|
+
* @param {object | null} board
|
|
322
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back' }} [options]
|
|
323
|
+
* @returns {object | null}
|
|
324
|
+
*/
|
|
325
|
+
export function preparePcbSideResolvedRenderModel(board, options = {}) {
|
|
326
|
+
return PcbSideResolvedRenderModel.resolve(board, options)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Checks whether a primitive belongs to an Altium copper signal layer.
|
|
331
|
+
* @param {object | null} primitive
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
export function isCopperPrimitive(primitive) {
|
|
335
|
+
return PcbSideResolvedRenderModel.isCopperPrimitive(primitive)
|
|
336
|
+
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { PcbArcUtils } from './PcbArcUtils.mjs'
|
|
6
6
|
import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs'
|
|
7
|
+
import { PcbEmbeddedFontFaceRenderer } from './PcbEmbeddedFontFaceRenderer.mjs'
|
|
7
8
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
9
|
+
import { PcbCopperPrimitiveSplitter } from './PcbCopperPrimitiveSplitter.mjs'
|
|
10
|
+
import { PcbRegionPrimitiveRenderer } from './PcbRegionPrimitiveRenderer.mjs'
|
|
11
|
+
import { PcbTextPrimitiveRenderer } from './PcbTextPrimitiveRenderer.mjs'
|
|
8
12
|
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
9
13
|
/**
|
|
10
14
|
* Renders normalized PCB models into HTML and SVG markup.
|
|
@@ -15,7 +19,7 @@ export class PcbSvgRenderer {
|
|
|
15
19
|
static #GENERIC_DETAIL_SEARCH_HALF_EXTENT = 240
|
|
16
20
|
/**
|
|
17
21
|
* Renders a normalized PCB model into HTML and SVG markup.
|
|
18
|
-
* @param {{ summary: { title?: string }, pcb?: { boardOutline: { segments: Array<Record<string, number | string>>, minX: number, minY: number, widthMil: number, heightMil: number }, layers: { name: string }[], primitiveLayers?: { layerId: number, name: string }[], 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 }[], vias?: { x: number, y: number, diameter: number, holeDiameter: number }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, shapeTop?: number, shapeMid?: number, shapeBottom?: number, rotation?: number, isPlated?: boolean }[], components: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] } }} documentModel
|
|
22
|
+
* @param {{ summary: { title?: string }, pcb?: { boardOutline: { segments: Array<Record<string, number | string>>, minX: number, minY: number, widthMil: number, heightMil: number }, layers: { name: string }[], primitiveLayers?: { layerId: number, name: string }[], 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 }[], vias?: { x: number, y: number, diameter: number, holeDiameter: number }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, shapeTop?: number, shapeMid?: number, shapeBottom?: number, rotation?: number, isPlated?: boolean }[], texts?: { text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, visible?: boolean }[], components: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] } }} documentModel
|
|
19
23
|
* @returns {string}
|
|
20
24
|
*/
|
|
21
25
|
static render(documentModel) {
|
|
@@ -28,14 +32,25 @@ export class PcbSvgRenderer {
|
|
|
28
32
|
const fills = pcb.fills || []
|
|
29
33
|
const tracks = pcb.tracks || []
|
|
30
34
|
const arcs = pcb.arcs || []
|
|
35
|
+
const regions = pcb.regions || []
|
|
36
|
+
const shapeBasedRegions = pcb.shapeBasedRegions || []
|
|
37
|
+
const renderedRegions = shapeBasedRegions.length
|
|
38
|
+
? shapeBasedRegions
|
|
39
|
+
: regions
|
|
31
40
|
const vias = pcb.vias || []
|
|
32
41
|
const pads = pcb.pads || []
|
|
33
42
|
const components = pcb.components.slice(0, 260)
|
|
34
|
-
const
|
|
43
|
+
const texts = PcbTextPrimitiveRenderer.select(
|
|
44
|
+
pcb.primitiveLayers || [],
|
|
45
|
+
pcb.texts || [],
|
|
46
|
+
'top'
|
|
47
|
+
)
|
|
48
|
+
const copperGroups = PcbCopperPrimitiveSplitter.split(
|
|
35
49
|
polygons,
|
|
36
50
|
fills,
|
|
37
51
|
tracks,
|
|
38
|
-
arcs
|
|
52
|
+
arcs,
|
|
53
|
+
renderedRegions
|
|
39
54
|
)
|
|
40
55
|
const footprintPrimitives = PcbEdgeFacingGlyphNormalizer.normalize(
|
|
41
56
|
PcbFootprintPrimitiveSelector.select(
|
|
@@ -43,6 +58,7 @@ export class PcbSvgRenderer {
|
|
|
43
58
|
fills,
|
|
44
59
|
tracks,
|
|
45
60
|
arcs,
|
|
61
|
+
renderedRegions,
|
|
46
62
|
'top'
|
|
47
63
|
),
|
|
48
64
|
outline
|
|
@@ -71,6 +87,11 @@ export class PcbSvgRenderer {
|
|
|
71
87
|
...copperGroups.subsurface.arcs,
|
|
72
88
|
...footprintPrimitives.arcs
|
|
73
89
|
],
|
|
90
|
+
[
|
|
91
|
+
...copperGroups.surface.regions,
|
|
92
|
+
...copperGroups.subsurface.regions,
|
|
93
|
+
...footprintPrimitives.regions
|
|
94
|
+
],
|
|
74
95
|
vias,
|
|
75
96
|
pads
|
|
76
97
|
)
|
|
@@ -151,6 +172,11 @@ export class PcbSvgRenderer {
|
|
|
151
172
|
)
|
|
152
173
|
)
|
|
153
174
|
.join('')
|
|
175
|
+
const regionMarkup = (regionList, visibilityClass) =>
|
|
176
|
+
PcbRegionPrimitiveRenderer.buildMarkup(
|
|
177
|
+
regionList,
|
|
178
|
+
'pcb-region pcb-region--' + visibilityClass
|
|
179
|
+
)
|
|
154
180
|
const viaMarkup = vias
|
|
155
181
|
.map((via) => {
|
|
156
182
|
const ringRadius = Math.max((via.diameter || 0) / 2, 1)
|
|
@@ -222,6 +248,14 @@ export class PcbSvgRenderer {
|
|
|
222
248
|
const footprintArcMarkup = footprintPrimitives.arcs
|
|
223
249
|
.map((arc) => PcbArcUtils.buildMarkup(arc, 'pcb-footprint-arc'))
|
|
224
250
|
.join('')
|
|
251
|
+
const footprintRegionMarkup = PcbRegionPrimitiveRenderer.buildMarkup(
|
|
252
|
+
footprintPrimitives.regions,
|
|
253
|
+
'pcb-footprint-region'
|
|
254
|
+
)
|
|
255
|
+
const textMarkup = PcbTextPrimitiveRenderer.render(texts)
|
|
256
|
+
const fontFaceMarkup = PcbEmbeddedFontFaceRenderer.buildMarkup(
|
|
257
|
+
pcb.embeddedFonts || []
|
|
258
|
+
)
|
|
225
259
|
|
|
226
260
|
const componentMarkup = components
|
|
227
261
|
.map((component) => {
|
|
@@ -260,13 +294,7 @@ export class PcbSvgRenderer {
|
|
|
260
294
|
SchematicSvgUtils.formatNumber(component.rotation) +
|
|
261
295
|
')">' +
|
|
262
296
|
bodyMarkup +
|
|
263
|
-
'
|
|
264
|
-
SchematicSvgUtils.formatNumber(
|
|
265
|
-
bodyGeometry.height * -0.75
|
|
266
|
-
) +
|
|
267
|
-
'">' +
|
|
268
|
-
SchematicSvgUtils.escapeHtml(component.designator) +
|
|
269
|
-
'</text></g>'
|
|
297
|
+
'</g>'
|
|
270
298
|
)
|
|
271
299
|
})
|
|
272
300
|
.join('')
|
|
@@ -288,7 +316,9 @@ export class PcbSvgRenderer {
|
|
|
288
316
|
'<svg class="pcb-svg" viewBox="' +
|
|
289
317
|
SchematicSvgUtils.escapeHtml(viewBox) +
|
|
290
318
|
'" preserveAspectRatio="xMidYMid meet" aria-label="PCB view">' +
|
|
291
|
-
'<defs
|
|
319
|
+
'<defs>' +
|
|
320
|
+
fontFaceMarkup +
|
|
321
|
+
'<clipPath id="' +
|
|
292
322
|
clipPathId +
|
|
293
323
|
'"><path d="' +
|
|
294
324
|
SchematicSvgUtils.escapeHtml(path) +
|
|
@@ -302,12 +332,14 @@ export class PcbSvgRenderer {
|
|
|
302
332
|
'<g class="pcb-copper pcb-copper--subsurface">' +
|
|
303
333
|
polygonMarkup(copperGroups.subsurface.polygons, 'subsurface') +
|
|
304
334
|
fillMarkup(copperGroups.subsurface.fills, 'subsurface') +
|
|
335
|
+
regionMarkup(copperGroups.subsurface.regions, 'subsurface') +
|
|
305
336
|
trackMarkup(copperGroups.subsurface.tracks, 'subsurface') +
|
|
306
337
|
arcMarkup(copperGroups.subsurface.arcs, 'subsurface') +
|
|
307
338
|
'</g>' +
|
|
308
339
|
'<g class="pcb-copper pcb-copper--surface">' +
|
|
309
340
|
polygonMarkup(copperGroups.surface.polygons, 'surface') +
|
|
310
341
|
fillMarkup(copperGroups.surface.fills, 'surface') +
|
|
342
|
+
regionMarkup(copperGroups.surface.regions, 'surface') +
|
|
311
343
|
trackMarkup(copperGroups.surface.tracks, 'surface') +
|
|
312
344
|
arcMarkup(copperGroups.surface.arcs, 'surface') +
|
|
313
345
|
padMarkup +
|
|
@@ -318,6 +350,12 @@ export class PcbSvgRenderer {
|
|
|
318
350
|
footprintFillMarkup +
|
|
319
351
|
footprintTrackMarkup +
|
|
320
352
|
footprintArcMarkup +
|
|
353
|
+
footprintRegionMarkup +
|
|
354
|
+
'</g>' +
|
|
355
|
+
'<g class="pcb-texts" clip-path="url(#' +
|
|
356
|
+
clipPathId +
|
|
357
|
+
')">' +
|
|
358
|
+
textMarkup +
|
|
321
359
|
'</g>' +
|
|
322
360
|
'<path class="board-outline board-outline--stroke" d="' +
|
|
323
361
|
SchematicSvgUtils.escapeHtml(path) +
|
|
@@ -382,6 +420,7 @@ export class PcbSvgRenderer {
|
|
|
382
420
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} fills
|
|
383
421
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} tracks
|
|
384
422
|
* @param {{ x: number, y: number, radius: number, width?: number }[]} arcs
|
|
423
|
+
* @param {{ points?: { x: number, y: number }[], holes?: { x: number, y: number }[][] }[]} regions
|
|
385
424
|
* @param {{ x: number, y: number, diameter: number }[]} vias
|
|
386
425
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number }[]} pads
|
|
387
426
|
* @returns {string}
|
|
@@ -393,6 +432,7 @@ export class PcbSvgRenderer {
|
|
|
393
432
|
fills,
|
|
394
433
|
tracks,
|
|
395
434
|
arcs,
|
|
435
|
+
regions,
|
|
396
436
|
vias,
|
|
397
437
|
pads
|
|
398
438
|
) {
|
|
@@ -424,6 +464,10 @@ export class PcbSvgRenderer {
|
|
|
424
464
|
PcbArcUtils.pushExtents(xs, ys, arc)
|
|
425
465
|
}
|
|
426
466
|
|
|
467
|
+
for (const region of regions) {
|
|
468
|
+
PcbRegionPrimitiveRenderer.pushExtents(xs, ys, region)
|
|
469
|
+
}
|
|
470
|
+
|
|
427
471
|
for (const via of vias) {
|
|
428
472
|
const radius = (via.diameter || 0) / 2
|
|
429
473
|
xs.push(via.x - radius, via.x + radius)
|
|
@@ -664,14 +708,24 @@ export class PcbSvgRenderer {
|
|
|
664
708
|
}
|
|
665
709
|
|
|
666
710
|
/**
|
|
667
|
-
* Returns true when one component already has authored
|
|
668
|
-
*
|
|
669
|
-
* @param {{ x: number, y: number, pattern: string }} component
|
|
670
|
-
* @param {{ fills: { x1: number, y1: number, x2: number, y2: number }[], tracks: { x1: number, y1: number, x2: number, y2: number }[], arcs: { x: number, y: number, radius: number, width?: number }[] }} footprintPrimitives
|
|
671
|
-
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, rotation?: number, offsetTopX?: number, offsetTopY?: number, holeDiameter?: number }[]} pads
|
|
711
|
+
* Returns true when one component already has authored geometry, either by
|
|
712
|
+
* an explicit component ownership link or by nearby recovered primitives.
|
|
713
|
+
* @param {{ componentIndex?: number | null, x: number, y: number, pattern: string }} component
|
|
714
|
+
* @param {{ fills: { componentIndex?: number | null, x1: number, y1: number, x2: number, y2: number }[], tracks: { componentIndex?: number | null, x1: number, y1: number, x2: number, y2: number }[], arcs: { componentIndex?: number | null, x: number, y: number, radius: number, width?: number }[], regions?: { componentIndex?: number | null, points?: { x: number, y: number }[], holes?: { x: number, y: number }[][] }[] }} footprintPrimitives
|
|
715
|
+
* @param {{ componentIndex?: number | null, x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, rotation?: number, offsetTopX?: number, offsetTopY?: number, holeDiameter?: number }[]} pads
|
|
672
716
|
* @returns {boolean}
|
|
673
717
|
*/
|
|
674
718
|
static #hasAuthoredFootprintDetail(component, footprintPrimitives, pads) {
|
|
719
|
+
if (
|
|
720
|
+
PcbSvgRenderer.#hasComponentOwnedFootprintDetail(
|
|
721
|
+
component,
|
|
722
|
+
footprintPrimitives,
|
|
723
|
+
pads
|
|
724
|
+
)
|
|
725
|
+
) {
|
|
726
|
+
return true
|
|
727
|
+
}
|
|
728
|
+
|
|
675
729
|
const bounds = PcbSvgRenderer.#footprintDetailBounds(component)
|
|
676
730
|
|
|
677
731
|
return (
|
|
@@ -684,12 +738,43 @@ export class PcbSvgRenderer {
|
|
|
684
738
|
(footprintPrimitives.arcs || []).some((arc) =>
|
|
685
739
|
PcbArcUtils.intersectsBounds(arc, bounds)
|
|
686
740
|
) ||
|
|
741
|
+
(footprintPrimitives.regions || []).some((region) =>
|
|
742
|
+
PcbRegionPrimitiveRenderer.intersectsBounds(region, bounds)
|
|
743
|
+
) ||
|
|
687
744
|
(pads || []).some((pad) =>
|
|
688
745
|
PcbSvgRenderer.#padIntersectsBounds(pad, bounds)
|
|
689
746
|
)
|
|
690
747
|
)
|
|
691
748
|
}
|
|
692
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Returns true when recovered primitives directly reference the component.
|
|
752
|
+
* @param {{ componentIndex?: number | null }} component
|
|
753
|
+
* @param {{ fills?: { componentIndex?: number | null }[], tracks?: { componentIndex?: number | null }[], arcs?: { componentIndex?: number | null }[], regions?: { componentIndex?: number | null }[] }} footprintPrimitives
|
|
754
|
+
* @param {{ componentIndex?: number | null }[]} pads
|
|
755
|
+
* @returns {boolean}
|
|
756
|
+
*/
|
|
757
|
+
static #hasComponentOwnedFootprintDetail(
|
|
758
|
+
component,
|
|
759
|
+
footprintPrimitives,
|
|
760
|
+
pads
|
|
761
|
+
) {
|
|
762
|
+
const componentIndex = Number(component?.componentIndex)
|
|
763
|
+
if (!Number.isInteger(componentIndex)) {
|
|
764
|
+
return false
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return [
|
|
768
|
+
...(footprintPrimitives.tracks || []),
|
|
769
|
+
...(footprintPrimitives.fills || []),
|
|
770
|
+
...(footprintPrimitives.arcs || []),
|
|
771
|
+
...(footprintPrimitives.regions || []),
|
|
772
|
+
...(pads || [])
|
|
773
|
+
].some(
|
|
774
|
+
(primitive) => Number(primitive?.componentIndex) === componentIndex
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
|
|
693
778
|
/**
|
|
694
779
|
* Returns true when one recovered pad surface overlaps a component-local
|
|
695
780
|
* search box, which means the footprint already has concrete 2D items.
|
|
@@ -771,89 +856,6 @@ export class PcbSvgRenderer {
|
|
|
771
856
|
)
|
|
772
857
|
}
|
|
773
858
|
|
|
774
|
-
/**
|
|
775
|
-
* Splits recovered copper primitives into the default top-facing surface
|
|
776
|
-
* view and de-emphasized buried layers.
|
|
777
|
-
* @param {{ layer?: string, segments: Array<Record<string, number | string>> }[]} polygons
|
|
778
|
-
* @param {{ x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[]} fills
|
|
779
|
-
* @param {{ x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[]} tracks
|
|
780
|
-
* @param {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[]} arcs
|
|
781
|
-
* @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 }[] }, 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 }[] } }}
|
|
782
|
-
*/
|
|
783
|
-
static #splitCopperPrimitives(polygons, fills, tracks, arcs) {
|
|
784
|
-
const copperFills = fills.filter((fill) =>
|
|
785
|
-
PcbSvgRenderer.#isCopperLayerId(fill.layerId)
|
|
786
|
-
)
|
|
787
|
-
const copperTracks = tracks.filter((track) =>
|
|
788
|
-
PcbSvgRenderer.#isCopperLayerId(track.layerId)
|
|
789
|
-
)
|
|
790
|
-
const copperArcs = arcs.filter((arc) =>
|
|
791
|
-
PcbSvgRenderer.#isCopperLayerId(arc.layerId)
|
|
792
|
-
)
|
|
793
|
-
const surfaceTrackLayerCode =
|
|
794
|
-
PcbSvgRenderer.#resolveSurfaceLayerCode(copperTracks)
|
|
795
|
-
const surfaceFillLayerCode =
|
|
796
|
-
PcbSvgRenderer.#resolveSurfaceLayerCode(copperFills)
|
|
797
|
-
const surfaceArcLayerCode =
|
|
798
|
-
PcbSvgRenderer.#resolveSurfaceLayerCode(copperArcs)
|
|
799
|
-
|
|
800
|
-
return {
|
|
801
|
-
surface: {
|
|
802
|
-
polygons: polygons.filter((polygon) =>
|
|
803
|
-
PcbSvgRenderer.#isSurfacePolygon(polygon)
|
|
804
|
-
),
|
|
805
|
-
fills: copperFills.filter(
|
|
806
|
-
(fill) => fill.layerCode === surfaceFillLayerCode
|
|
807
|
-
),
|
|
808
|
-
tracks: copperTracks.filter(
|
|
809
|
-
(track) => track.layerCode === surfaceTrackLayerCode
|
|
810
|
-
),
|
|
811
|
-
arcs: copperArcs.filter(
|
|
812
|
-
(arc) => arc.layerCode === surfaceArcLayerCode
|
|
813
|
-
)
|
|
814
|
-
},
|
|
815
|
-
subsurface: {
|
|
816
|
-
polygons: polygons.filter(
|
|
817
|
-
(polygon) => !PcbSvgRenderer.#isSurfacePolygon(polygon)
|
|
818
|
-
),
|
|
819
|
-
fills: copperFills.filter(
|
|
820
|
-
(fill) => fill.layerCode !== surfaceFillLayerCode
|
|
821
|
-
),
|
|
822
|
-
tracks: copperTracks.filter(
|
|
823
|
-
(track) => track.layerCode !== surfaceTrackLayerCode
|
|
824
|
-
),
|
|
825
|
-
arcs: copperArcs.filter(
|
|
826
|
-
(arc) => arc.layerCode !== surfaceArcLayerCode
|
|
827
|
-
)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Returns the default visible layer code from one primitive family.
|
|
834
|
-
* @param {{ layerCode?: number }[]} primitives
|
|
835
|
-
* @returns {number | null}
|
|
836
|
-
*/
|
|
837
|
-
static #resolveSurfaceLayerCode(primitives) {
|
|
838
|
-
const layerCodes = primitives
|
|
839
|
-
.map((primitive) => primitive.layerCode)
|
|
840
|
-
.filter((layerCode) => Number.isFinite(layerCode))
|
|
841
|
-
return layerCodes.length ? Math.min(...layerCodes) : null
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* Returns true when one polygon belongs to the top-facing copper view.
|
|
846
|
-
* @param {{ layer?: string }} polygon
|
|
847
|
-
* @returns {boolean}
|
|
848
|
-
*/
|
|
849
|
-
static #isSurfacePolygon(polygon) {
|
|
850
|
-
return (
|
|
851
|
-
String(polygon.layer || '')
|
|
852
|
-
.trim()
|
|
853
|
-
.toUpperCase() === 'TOP'
|
|
854
|
-
)
|
|
855
|
-
}
|
|
856
|
-
|
|
857
859
|
/**
|
|
858
860
|
* Returns true when one track intersects a component-local search box.
|
|
859
861
|
* @param {{ x1: number, y1: number, x2: number, y2: number }} track
|
|
@@ -893,14 +895,4 @@ export class PcbSvgRenderer {
|
|
|
893
895
|
minY > bounds.maxY
|
|
894
896
|
)
|
|
895
897
|
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Returns true when one decoded primitive layer belongs to the copper
|
|
899
|
-
* stack instead of a mechanical or annotation layer.
|
|
900
|
-
* @param {number | undefined} layerId
|
|
901
|
-
* @returns {boolean}
|
|
902
|
-
*/
|
|
903
|
-
static #isCopperLayerId(layerId) {
|
|
904
|
-
return Number.isInteger(layerId) && layerId >= 1 && layerId <= 32
|
|
905
|
-
}
|
|
906
898
|
}
|