altium-toolkit 1.1.40 → 1.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/altium/AltiumLayoutParser.mjs +9 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +1 -0
- package/src/styles/altium-renderers.css +67 -1
- package/src/ui/PcbInteractionLayerModel.mjs +6 -2
- package/src/ui/PcbPadMaskApertureRenderer.mjs +288 -0
- package/src/ui/PcbSvgRenderer.mjs +674 -38
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
import { PcbLayerIdCodec } from './PcbLayerIdCodec.mjs'
|
|
6
7
|
import { SchematicTextParser } from './SchematicTextParser.mjs'
|
|
7
8
|
|
|
8
9
|
const { getField, parseNumericField } = ParserUtils
|
|
@@ -114,14 +115,18 @@ export class AltiumLayoutParser {
|
|
|
114
115
|
if (!match) continue
|
|
115
116
|
|
|
116
117
|
const index = Number.parseInt(match[1], 10)
|
|
118
|
+
const layerId = parseNumericField(
|
|
119
|
+
fields,
|
|
120
|
+
'V9_STACK_LAYER' + index + '_LAYERID'
|
|
121
|
+
)
|
|
122
|
+
const legacyLayerId =
|
|
123
|
+
PcbLayerIdCodec.legacyLayerIdFromV7SaveId(layerId) ?? undefined
|
|
117
124
|
layers.push(
|
|
118
125
|
AltiumLayoutParser.#stripUndefined({
|
|
119
126
|
index,
|
|
120
127
|
name: getField(fields, key),
|
|
121
|
-
layerId
|
|
122
|
-
|
|
123
|
-
'V9_STACK_LAYER' + index + '_LAYERID'
|
|
124
|
-
),
|
|
128
|
+
layerId,
|
|
129
|
+
legacyLayerId,
|
|
125
130
|
kind: AltiumLayoutParser.#firstLayerStackTextField(
|
|
126
131
|
fields,
|
|
127
132
|
index,
|
|
@@ -180,6 +180,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
180
180
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
181
181
|
index: layer.index,
|
|
182
182
|
layerId: layer.layerId,
|
|
183
|
+
legacyLayerId: layer.legacyLayerId,
|
|
183
184
|
layerKey: Number.isFinite(layer.layerId)
|
|
184
185
|
? 'L' + layer.layerId
|
|
185
186
|
: undefined,
|
|
@@ -142,6 +142,16 @@
|
|
|
142
142
|
--pcb-via-hole-fill: #0f746c;
|
|
143
143
|
--pcb-footprint-fill: rgba(247, 230, 117, 0.14);
|
|
144
144
|
--pcb-footprint-track-color: rgba(237, 172, 36, 1);
|
|
145
|
+
--pcb-detail-paste-fill: rgba(146, 146, 146, 0.94);
|
|
146
|
+
--pcb-detail-paste-stroke: rgba(154, 154, 154, 0.96);
|
|
147
|
+
--pcb-detail-mask-fill: rgba(137, 34, 132, 0.4);
|
|
148
|
+
--pcb-detail-mask-stroke: rgba(137, 34, 132, 0.82);
|
|
149
|
+
--pcb-detail-mechanical-fill: rgba(148, 160, 166, 0.32);
|
|
150
|
+
--pcb-detail-mechanical-stroke: rgba(96, 110, 118, 0.86);
|
|
151
|
+
--pcb-detail-drill-fill: rgba(79, 87, 93, 0.46);
|
|
152
|
+
--pcb-detail-drill-stroke: rgba(42, 50, 56, 0.88);
|
|
153
|
+
--pcb-detail-fill: rgba(150, 160, 166, 0.38);
|
|
154
|
+
--pcb-detail-stroke: rgba(80, 92, 100, 0.82);
|
|
145
155
|
--pcb-dimension-color: rgba(39, 88, 111, 0.78);
|
|
146
156
|
--pcb-dimension-text: #22343b;
|
|
147
157
|
--pcb-text-knockout-fill: rgba(248, 246, 239, 0.96);
|
|
@@ -200,7 +210,9 @@
|
|
|
200
210
|
.pcb-track,
|
|
201
211
|
.pcb-arc,
|
|
202
212
|
.pcb-footprint-track,
|
|
203
|
-
.pcb-footprint-arc
|
|
213
|
+
.pcb-footprint-arc,
|
|
214
|
+
.pcb-detail-track,
|
|
215
|
+
.pcb-detail-arc {
|
|
204
216
|
fill: none;
|
|
205
217
|
stroke-linecap: round;
|
|
206
218
|
stroke-linejoin: round;
|
|
@@ -271,6 +283,60 @@
|
|
|
271
283
|
stroke: var(--pcb-footprint-track-color);
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
.pcb-detail-fill,
|
|
287
|
+
.pcb-detail-region {
|
|
288
|
+
fill: var(--pcb-detail-fill);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.pcb-detail-track,
|
|
292
|
+
.pcb-detail-arc {
|
|
293
|
+
stroke: var(--pcb-detail-stroke);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.pcb-detail-fill--paste,
|
|
297
|
+
.pcb-detail-region--paste {
|
|
298
|
+
fill: var(--pcb-detail-paste-fill);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.pcb-detail-track--paste,
|
|
302
|
+
.pcb-detail-arc--paste {
|
|
303
|
+
stroke: var(--pcb-detail-paste-stroke);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.pcb-detail-fill--mask,
|
|
307
|
+
.pcb-detail-region--mask,
|
|
308
|
+
.pcb-detail-fill--solder-mask,
|
|
309
|
+
.pcb-detail-region--solder-mask {
|
|
310
|
+
fill: var(--pcb-detail-mask-fill);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.pcb-detail-track--mask,
|
|
314
|
+
.pcb-detail-arc--mask,
|
|
315
|
+
.pcb-detail-track--solder-mask,
|
|
316
|
+
.pcb-detail-arc--solder-mask {
|
|
317
|
+
stroke: var(--pcb-detail-mask-stroke);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pcb-detail-fill--mechanical,
|
|
321
|
+
.pcb-detail-region--mechanical {
|
|
322
|
+
fill: var(--pcb-detail-mechanical-fill);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.pcb-detail-track--mechanical,
|
|
326
|
+
.pcb-detail-arc--mechanical {
|
|
327
|
+
stroke: var(--pcb-detail-mechanical-stroke);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.pcb-detail-fill--drill,
|
|
331
|
+
.pcb-detail-region--drill {
|
|
332
|
+
fill: var(--pcb-detail-drill-fill);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.pcb-detail-track--drill,
|
|
336
|
+
.pcb-detail-arc--drill {
|
|
337
|
+
stroke: var(--pcb-detail-drill-stroke);
|
|
338
|
+
}
|
|
339
|
+
|
|
274
340
|
.pcb-dimensions {
|
|
275
341
|
pointer-events: none;
|
|
276
342
|
}
|
|
@@ -56,13 +56,17 @@ export class PcbInteractionLayerModel {
|
|
|
56
56
|
const key = String(layer?.name || layer?.layer || '').trim()
|
|
57
57
|
if (!key || seen.has(key)) continue
|
|
58
58
|
seen.add(key)
|
|
59
|
-
|
|
59
|
+
const row = {
|
|
60
60
|
key,
|
|
61
61
|
label: key,
|
|
62
62
|
layerId: Number.isFinite(Number(layer?.layerId))
|
|
63
63
|
? Number(layer.layerId)
|
|
64
64
|
: null
|
|
65
|
-
}
|
|
65
|
+
}
|
|
66
|
+
if (Number.isFinite(Number(layer?.legacyLayerId))) {
|
|
67
|
+
row.legacyLayerId = Number(layer.legacyLayerId)
|
|
68
|
+
}
|
|
69
|
+
layers.push(row)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
return layers
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders pad-derived paste and solder-mask apertures as independent SVG layers.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbPadMaskApertureRenderer {
|
|
11
|
+
static #PAD_SHAPE_RECTANGULAR = 2
|
|
12
|
+
static #SPECS = [
|
|
13
|
+
{
|
|
14
|
+
flag: 'hasTopPasteMaskOpening',
|
|
15
|
+
kind: 'paste',
|
|
16
|
+
primitiveKind: 'pad-paste',
|
|
17
|
+
side: 'top',
|
|
18
|
+
layerId: 35,
|
|
19
|
+
layerName: 'Top Paste',
|
|
20
|
+
expansionKey: 'effectivePasteMaskExpansion'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
flag: 'hasBottomPasteMaskOpening',
|
|
24
|
+
kind: 'paste',
|
|
25
|
+
primitiveKind: 'pad-paste',
|
|
26
|
+
side: 'bottom',
|
|
27
|
+
layerId: 36,
|
|
28
|
+
layerName: 'Bottom Paste',
|
|
29
|
+
expansionKey: 'effectivePasteMaskExpansion'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
flag: 'hasTopSolderMaskOpening',
|
|
33
|
+
kind: 'solder-mask',
|
|
34
|
+
primitiveKind: 'pad-solder-mask',
|
|
35
|
+
side: 'top',
|
|
36
|
+
layerId: 37,
|
|
37
|
+
layerName: 'Top Solder',
|
|
38
|
+
expansionKey: 'effectiveSolderMaskExpansion'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: 'hasBottomSolderMaskOpening',
|
|
42
|
+
kind: 'solder-mask',
|
|
43
|
+
primitiveKind: 'pad-solder-mask',
|
|
44
|
+
side: 'bottom',
|
|
45
|
+
layerId: 38,
|
|
46
|
+
layerName: 'Bottom Solder',
|
|
47
|
+
expansionKey: 'effectiveSolderMaskExpansion'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds one SVG group containing all derived pad mask apertures.
|
|
53
|
+
* @param {object[]} pads Normalized pad records.
|
|
54
|
+
* @param {{ attributes?: (aperture: object) => string, padIndex?: (pad: object, index: number) => number }} [options]
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
static render(pads, options = {}) {
|
|
58
|
+
const apertures = []
|
|
59
|
+
|
|
60
|
+
for (const [fallbackIndex, pad] of (pads || []).entries()) {
|
|
61
|
+
const padIndex = PcbPadMaskApertureRenderer.#padIndex(
|
|
62
|
+
pad,
|
|
63
|
+
fallbackIndex,
|
|
64
|
+
options
|
|
65
|
+
)
|
|
66
|
+
for (const [specIndex, spec] of this.#SPECS.entries()) {
|
|
67
|
+
if (!pad?.[spec.flag]) continue
|
|
68
|
+
const aperture = this.#apertureForPad(
|
|
69
|
+
pad,
|
|
70
|
+
padIndex,
|
|
71
|
+
specIndex,
|
|
72
|
+
spec
|
|
73
|
+
)
|
|
74
|
+
if (!aperture) continue
|
|
75
|
+
apertures.push(
|
|
76
|
+
this.#renderAperture(aperture, options.attributes)
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!apertures.length) return ''
|
|
82
|
+
|
|
83
|
+
return '<g class="pcb-pad-mask-layers">' + apertures.join('') + '</g>'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolves one stable pad index.
|
|
88
|
+
* @param {object} pad Pad record.
|
|
89
|
+
* @param {number} fallbackIndex Array index.
|
|
90
|
+
* @param {{ padIndex?: (pad: object, index: number) => number }} options
|
|
91
|
+
* @returns {number}
|
|
92
|
+
*/
|
|
93
|
+
static #padIndex(pad, fallbackIndex, options) {
|
|
94
|
+
const resolved = options.padIndex?.(pad, fallbackIndex)
|
|
95
|
+
return Number.isInteger(resolved) ? resolved : fallbackIndex
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds one normalized aperture descriptor for a pad/spec pair.
|
|
100
|
+
* @param {object} pad Pad record.
|
|
101
|
+
* @param {number} padIndex Stable pad index.
|
|
102
|
+
* @param {number} specIndex Spec index.
|
|
103
|
+
* @param {object} spec Aperture layer spec.
|
|
104
|
+
* @returns {object | null}
|
|
105
|
+
*/
|
|
106
|
+
static #apertureForPad(pad, padIndex, specIndex, spec) {
|
|
107
|
+
const size = this.#sizeForPad(pad, spec)
|
|
108
|
+
if (size.width <= 0 || size.height <= 0) return null
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
pad,
|
|
112
|
+
padIndex,
|
|
113
|
+
index: padIndex * this.#SPECS.length + specIndex,
|
|
114
|
+
spec,
|
|
115
|
+
size,
|
|
116
|
+
primitive: {
|
|
117
|
+
...pad,
|
|
118
|
+
layerId: spec.layerId,
|
|
119
|
+
layerCode: spec.layerId,
|
|
120
|
+
legacyLayerId: spec.layerId,
|
|
121
|
+
layerName: spec.layerName
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolves aperture dimensions after the parsed mask expansion.
|
|
128
|
+
* @param {object} pad Pad record.
|
|
129
|
+
* @param {object} spec Aperture layer spec.
|
|
130
|
+
* @returns {{ width: number, height: number }}
|
|
131
|
+
*/
|
|
132
|
+
static #sizeForPad(pad, spec) {
|
|
133
|
+
const side = spec.side === 'bottom' ? 'Bottom' : 'Top'
|
|
134
|
+
const width = Number(pad?.['size' + side + 'X'] || pad?.sizeMidX || 0)
|
|
135
|
+
const height = Number(pad?.['size' + side + 'Y'] || pad?.sizeMidY || 0)
|
|
136
|
+
const expansion = Number(pad?.[spec.expansionKey] || 0)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
width: Math.max(width + expansion * 2, 0),
|
|
140
|
+
height: Math.max(height + expansion * 2, 0)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Renders one aperture shape.
|
|
146
|
+
* @param {object} aperture Aperture descriptor.
|
|
147
|
+
* @param {(aperture: object) => string | undefined} attributes Attribute callback.
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
static #renderAperture(aperture, attributes) {
|
|
151
|
+
const className =
|
|
152
|
+
'pcb-detail-fill pcb-detail-fill--' +
|
|
153
|
+
(aperture.spec.kind === 'solder-mask' ? 'mask' : 'paste') +
|
|
154
|
+
' pcb-pad-mask-aperture pcb-pad-mask-aperture--' +
|
|
155
|
+
aperture.spec.kind
|
|
156
|
+
const renderedAttributes = attributes?.(aperture) || ''
|
|
157
|
+
|
|
158
|
+
return PcbPadMaskApertureRenderer.#isRoundAperture(aperture)
|
|
159
|
+
? PcbPadMaskApertureRenderer.#renderCircle(
|
|
160
|
+
aperture,
|
|
161
|
+
className,
|
|
162
|
+
renderedAttributes
|
|
163
|
+
)
|
|
164
|
+
: PcbPadMaskApertureRenderer.#renderRect(
|
|
165
|
+
aperture,
|
|
166
|
+
className,
|
|
167
|
+
renderedAttributes
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Renders one circular aperture.
|
|
173
|
+
* @param {object} aperture Aperture descriptor.
|
|
174
|
+
* @param {string} className SVG class list.
|
|
175
|
+
* @param {string} attributes SVG attributes.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static #renderCircle(aperture, className, attributes) {
|
|
179
|
+
const { pad, size } = aperture
|
|
180
|
+
const radius = Math.max(size.width, size.height) / 2
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
'<circle class="' +
|
|
184
|
+
className +
|
|
185
|
+
'" cx="' +
|
|
186
|
+
SchematicSvgUtils.formatNumber(Number(pad.x || 0)) +
|
|
187
|
+
'" cy="' +
|
|
188
|
+
SchematicSvgUtils.formatNumber(Number(pad.y || 0)) +
|
|
189
|
+
'" r="' +
|
|
190
|
+
SchematicSvgUtils.formatNumber(radius) +
|
|
191
|
+
'"' +
|
|
192
|
+
attributes +
|
|
193
|
+
' />'
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Renders one rectangular aperture.
|
|
199
|
+
* @param {object} aperture Aperture descriptor.
|
|
200
|
+
* @param {string} className SVG class list.
|
|
201
|
+
* @param {string} attributes SVG attributes.
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
static #renderRect(aperture, className, attributes) {
|
|
205
|
+
const { pad, size } = aperture
|
|
206
|
+
const x = Number(pad.x || 0) - size.width / 2
|
|
207
|
+
const y = Number(pad.y || 0) - size.height / 2
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
'<rect class="' +
|
|
211
|
+
className +
|
|
212
|
+
'" x="' +
|
|
213
|
+
SchematicSvgUtils.formatNumber(x) +
|
|
214
|
+
'" y="' +
|
|
215
|
+
SchematicSvgUtils.formatNumber(y) +
|
|
216
|
+
'" width="' +
|
|
217
|
+
SchematicSvgUtils.formatNumber(size.width) +
|
|
218
|
+
'" height="' +
|
|
219
|
+
SchematicSvgUtils.formatNumber(size.height) +
|
|
220
|
+
'" rx="' +
|
|
221
|
+
SchematicSvgUtils.formatNumber(
|
|
222
|
+
PcbPadMaskApertureRenderer.#cornerRadius(aperture)
|
|
223
|
+
) +
|
|
224
|
+
'" transform="rotate(' +
|
|
225
|
+
SchematicSvgUtils.formatNumber(Number(pad.rotation || 0)) +
|
|
226
|
+
' ' +
|
|
227
|
+
SchematicSvgUtils.formatNumber(Number(pad.x || 0)) +
|
|
228
|
+
' ' +
|
|
229
|
+
SchematicSvgUtils.formatNumber(Number(pad.y || 0)) +
|
|
230
|
+
')"' +
|
|
231
|
+
attributes +
|
|
232
|
+
' />'
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns true when one aperture should be rendered as a circle.
|
|
238
|
+
* @param {object} aperture Aperture descriptor.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
static #isRoundAperture(aperture) {
|
|
242
|
+
const shape = PcbPadMaskApertureRenderer.#shapeForAperture(aperture)
|
|
243
|
+
return (
|
|
244
|
+
shape !== PcbPadMaskApertureRenderer.#PAD_SHAPE_RECTANGULAR &&
|
|
245
|
+
Math.abs(aperture.size.width - aperture.size.height) < 0.001
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolves the side-specific pad shape code.
|
|
251
|
+
* @param {object} aperture Aperture descriptor.
|
|
252
|
+
* @returns {number}
|
|
253
|
+
*/
|
|
254
|
+
static #shapeForAperture(aperture) {
|
|
255
|
+
const side = aperture.spec.side === 'bottom' ? 'Bottom' : 'Top'
|
|
256
|
+
if (
|
|
257
|
+
side === 'Top' &&
|
|
258
|
+
aperture.pad?.hasRoundedRect &&
|
|
259
|
+
Number.isInteger(aperture.pad.roundedRectShapeTop)
|
|
260
|
+
) {
|
|
261
|
+
return Number(aperture.pad.roundedRectShapeTop)
|
|
262
|
+
}
|
|
263
|
+
return Number(aperture.pad?.['shape' + side] || 0)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolves rectangle corner radius for rounded pad shapes.
|
|
268
|
+
* @param {object} aperture Aperture descriptor.
|
|
269
|
+
* @returns {number}
|
|
270
|
+
*/
|
|
271
|
+
static #cornerRadius(aperture) {
|
|
272
|
+
if (
|
|
273
|
+
aperture.spec.side === 'top' &&
|
|
274
|
+
aperture.pad?.hasRoundedRect &&
|
|
275
|
+
Number.isFinite(aperture.pad.cornerRadiusTop)
|
|
276
|
+
) {
|
|
277
|
+
return (
|
|
278
|
+
Math.min(aperture.size.width, aperture.size.height) *
|
|
279
|
+
(Number(aperture.pad.cornerRadiusTop) / 100)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
if (PcbPadMaskApertureRenderer.#shapeForAperture(aperture) === 1) {
|
|
283
|
+
return Math.min(aperture.size.width, aperture.size.height) / 2
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 0
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -8,7 +8,10 @@ import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs
|
|
|
8
8
|
import { PcbEmbeddedFontFaceRenderer } from './PcbEmbeddedFontFaceRenderer.mjs'
|
|
9
9
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
10
10
|
import { PcbCopperPrimitiveSplitter } from './PcbCopperPrimitiveSplitter.mjs'
|
|
11
|
+
import { PcbLayerIdCodec } from '../core/altium/PcbLayerIdCodec.mjs'
|
|
12
|
+
import { PcbLayerGroups } from '../core/altium/PcbLayerGroups.mjs'
|
|
11
13
|
import { PcbNativeTextKnockoutDetector } from './PcbNativeTextKnockoutDetector.mjs'
|
|
14
|
+
import { PcbPadMaskApertureRenderer } from './PcbPadMaskApertureRenderer.mjs'
|
|
12
15
|
import { PcbRegionPrimitiveRenderer } from './PcbRegionPrimitiveRenderer.mjs'
|
|
13
16
|
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
14
17
|
import { PcbTextPrimitiveRenderer } from './PcbTextPrimitiveRenderer.mjs'
|
|
@@ -80,7 +83,7 @@ export class PcbSvgRenderer {
|
|
|
80
83
|
arcs,
|
|
81
84
|
renderedRegions
|
|
82
85
|
)
|
|
83
|
-
const
|
|
86
|
+
const selectedFootprintPrimitives =
|
|
84
87
|
PcbFootprintPrimitiveSelector.select(
|
|
85
88
|
primitiveLayers,
|
|
86
89
|
fills,
|
|
@@ -88,9 +91,21 @@ export class PcbSvgRenderer {
|
|
|
88
91
|
arcs,
|
|
89
92
|
renderedRegions,
|
|
90
93
|
'top'
|
|
91
|
-
)
|
|
94
|
+
)
|
|
95
|
+
const footprintPrimitives = PcbEdgeFacingGlyphNormalizer.normalize(
|
|
96
|
+
selectedFootprintPrimitives,
|
|
92
97
|
outline
|
|
93
98
|
)
|
|
99
|
+
const detailPrimitives = PcbSvgRenderer.#detailPrimitiveCollections(
|
|
100
|
+
{
|
|
101
|
+
fills,
|
|
102
|
+
tracks,
|
|
103
|
+
arcs,
|
|
104
|
+
regions: renderedRegions
|
|
105
|
+
},
|
|
106
|
+
PcbSvgRenderer.#primitiveIdentitySet(selectedFootprintPrimitives),
|
|
107
|
+
semanticContext
|
|
108
|
+
)
|
|
94
109
|
const texts = PcbTextPrimitiveRenderer.select(
|
|
95
110
|
primitiveLayers,
|
|
96
111
|
pcb.texts || [],
|
|
@@ -115,22 +130,26 @@ export class PcbSvgRenderer {
|
|
|
115
130
|
[
|
|
116
131
|
...copperGroups.surface.fills,
|
|
117
132
|
...copperGroups.subsurface.fills,
|
|
118
|
-
...footprintPrimitives.fills
|
|
133
|
+
...footprintPrimitives.fills,
|
|
134
|
+
...detailPrimitives.fills
|
|
119
135
|
],
|
|
120
136
|
[
|
|
121
137
|
...copperGroups.surface.tracks,
|
|
122
138
|
...copperGroups.subsurface.tracks,
|
|
123
|
-
...footprintPrimitives.tracks
|
|
139
|
+
...footprintPrimitives.tracks,
|
|
140
|
+
...detailPrimitives.tracks
|
|
124
141
|
],
|
|
125
142
|
[
|
|
126
143
|
...copperGroups.surface.arcs,
|
|
127
144
|
...copperGroups.subsurface.arcs,
|
|
128
|
-
...footprintPrimitives.arcs
|
|
145
|
+
...footprintPrimitives.arcs,
|
|
146
|
+
...detailPrimitives.arcs
|
|
129
147
|
],
|
|
130
148
|
[
|
|
131
149
|
...copperGroups.surface.regions,
|
|
132
150
|
...copperGroups.subsurface.regions,
|
|
133
|
-
...footprintPrimitives.regions
|
|
151
|
+
...footprintPrimitives.regions,
|
|
152
|
+
...detailPrimitives.regions
|
|
134
153
|
],
|
|
135
154
|
vias,
|
|
136
155
|
pads,
|
|
@@ -263,10 +282,27 @@ export class PcbSvgRenderer {
|
|
|
263
282
|
)
|
|
264
283
|
.join('')
|
|
265
284
|
const regionMarkup = (regionList, visibilityClass) =>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
regionList
|
|
286
|
+
.map((region, index) =>
|
|
287
|
+
PcbSvgRenderer.#appendSvgAttributes(
|
|
288
|
+
PcbRegionPrimitiveRenderer.buildMarkup(
|
|
289
|
+
[region],
|
|
290
|
+
'pcb-region pcb-region--' + visibilityClass
|
|
291
|
+
),
|
|
292
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
293
|
+
'region',
|
|
294
|
+
region,
|
|
295
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
296
|
+
semanticContext,
|
|
297
|
+
'regions',
|
|
298
|
+
region,
|
|
299
|
+
index
|
|
300
|
+
),
|
|
301
|
+
semanticContext
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
.join('')
|
|
270
306
|
const viaMarkup = vias
|
|
271
307
|
.map((via, index) => {
|
|
272
308
|
const ringRadius = Math.max((via.diameter || 0) / 2, 1)
|
|
@@ -328,8 +364,26 @@ export class PcbSvgRenderer {
|
|
|
328
364
|
)
|
|
329
365
|
)
|
|
330
366
|
.join('')
|
|
367
|
+
const padMaskApertureMarkup = PcbPadMaskApertureRenderer.render(pads, {
|
|
368
|
+
padIndex: (pad, index) =>
|
|
369
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
370
|
+
semanticContext,
|
|
371
|
+
'pads',
|
|
372
|
+
pad,
|
|
373
|
+
index
|
|
374
|
+
),
|
|
375
|
+
attributes: (aperture) =>
|
|
376
|
+
PcbSvgRenderer.#padMaskApertureAttributes(
|
|
377
|
+
aperture,
|
|
378
|
+
semanticContext
|
|
379
|
+
)
|
|
380
|
+
})
|
|
381
|
+
const detailMarkup = PcbSvgRenderer.#renderDetailPrimitives(
|
|
382
|
+
detailPrimitives,
|
|
383
|
+
semanticContext
|
|
384
|
+
)
|
|
331
385
|
const footprintFillMarkup = footprintPrimitives.fills
|
|
332
|
-
.map((fill) => {
|
|
386
|
+
.map((fill, index) => {
|
|
333
387
|
const x = Math.min(fill.x1, fill.x2)
|
|
334
388
|
const y = Math.min(fill.y1, fill.y2)
|
|
335
389
|
const width = Math.abs(fill.x2 - fill.x1)
|
|
@@ -347,13 +401,25 @@ export class PcbSvgRenderer {
|
|
|
347
401
|
SchematicSvgUtils.formatNumber(
|
|
348
402
|
Math.min(width, height) / 6
|
|
349
403
|
) +
|
|
350
|
-
'"
|
|
404
|
+
'"' +
|
|
405
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
406
|
+
'fill',
|
|
407
|
+
fill,
|
|
408
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
409
|
+
semanticContext,
|
|
410
|
+
'fills',
|
|
411
|
+
fill,
|
|
412
|
+
index
|
|
413
|
+
),
|
|
414
|
+
semanticContext
|
|
415
|
+
) +
|
|
416
|
+
' />'
|
|
351
417
|
)
|
|
352
418
|
})
|
|
353
419
|
.join('')
|
|
354
420
|
const footprintTrackMarkup = footprintPrimitives.tracks
|
|
355
421
|
.map(
|
|
356
|
-
(track) =>
|
|
422
|
+
(track, index) =>
|
|
357
423
|
'<line class="pcb-footprint-track" x1="' +
|
|
358
424
|
SchematicSvgUtils.formatNumber(track.x1) +
|
|
359
425
|
'" y1="' +
|
|
@@ -366,16 +432,60 @@ export class PcbSvgRenderer {
|
|
|
366
432
|
SchematicSvgUtils.formatNumber(
|
|
367
433
|
Math.max(track.width || 0, 1)
|
|
368
434
|
) +
|
|
369
|
-
'"
|
|
435
|
+
'"' +
|
|
436
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
437
|
+
'track',
|
|
438
|
+
track,
|
|
439
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
440
|
+
semanticContext,
|
|
441
|
+
'tracks',
|
|
442
|
+
track,
|
|
443
|
+
index
|
|
444
|
+
),
|
|
445
|
+
semanticContext
|
|
446
|
+
) +
|
|
447
|
+
' />'
|
|
370
448
|
)
|
|
371
449
|
.join('')
|
|
372
450
|
const footprintArcMarkup = footprintPrimitives.arcs
|
|
373
|
-
.map((arc) =>
|
|
451
|
+
.map((arc, index) =>
|
|
452
|
+
PcbSvgRenderer.#appendSvgAttributes(
|
|
453
|
+
PcbArcUtils.buildMarkup(arc, 'pcb-footprint-arc'),
|
|
454
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
455
|
+
'arc',
|
|
456
|
+
arc,
|
|
457
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
458
|
+
semanticContext,
|
|
459
|
+
'arcs',
|
|
460
|
+
arc,
|
|
461
|
+
index
|
|
462
|
+
),
|
|
463
|
+
semanticContext
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
.join('')
|
|
468
|
+
const footprintRegionMarkup = footprintPrimitives.regions
|
|
469
|
+
.map((region, index) =>
|
|
470
|
+
PcbSvgRenderer.#appendSvgAttributes(
|
|
471
|
+
PcbRegionPrimitiveRenderer.buildMarkup(
|
|
472
|
+
[region],
|
|
473
|
+
'pcb-footprint-region'
|
|
474
|
+
),
|
|
475
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
476
|
+
'region',
|
|
477
|
+
region,
|
|
478
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
479
|
+
semanticContext,
|
|
480
|
+
'regions',
|
|
481
|
+
region,
|
|
482
|
+
index
|
|
483
|
+
),
|
|
484
|
+
semanticContext
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
)
|
|
374
488
|
.join('')
|
|
375
|
-
const footprintRegionMarkup = PcbRegionPrimitiveRenderer.buildMarkup(
|
|
376
|
-
footprintPrimitives.regions,
|
|
377
|
-
'pcb-footprint-region'
|
|
378
|
-
)
|
|
379
489
|
const textMarkup = PcbTextPrimitiveRenderer.render(texts, {
|
|
380
490
|
semanticContext
|
|
381
491
|
})
|
|
@@ -519,6 +629,7 @@ export class PcbSvgRenderer {
|
|
|
519
629
|
SchematicSvgUtils.escapeHtml(path) +
|
|
520
630
|
'"' +
|
|
521
631
|
' />' +
|
|
632
|
+
padMaskApertureMarkup +
|
|
522
633
|
'<g class="pcb-copper-layers" clip-path="url(#' +
|
|
523
634
|
clipPathId +
|
|
524
635
|
')">' +
|
|
@@ -545,6 +656,7 @@ export class PcbSvgRenderer {
|
|
|
545
656
|
footprintArcMarkup +
|
|
546
657
|
footprintRegionMarkup +
|
|
547
658
|
'</g>' +
|
|
659
|
+
detailMarkup +
|
|
548
660
|
'<g class="pcb-components">' +
|
|
549
661
|
componentMarkup +
|
|
550
662
|
'</g>' +
|
|
@@ -594,6 +706,316 @@ export class PcbSvgRenderer {
|
|
|
594
706
|
)
|
|
595
707
|
}
|
|
596
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Selects non-copper layer primitives that need explicit SVG artwork.
|
|
711
|
+
* @param {{ fills?: object[], tracks?: object[], arcs?: object[], regions?: object[] }} primitives Primitive collections.
|
|
712
|
+
* @param {Set<object>} excludedPrimitives Primitives already rendered elsewhere.
|
|
713
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
714
|
+
* @returns {{ fills: object[], tracks: object[], arcs: object[], regions: object[] }}
|
|
715
|
+
*/
|
|
716
|
+
static #detailPrimitiveCollections(
|
|
717
|
+
primitives,
|
|
718
|
+
excludedPrimitives,
|
|
719
|
+
semanticContext
|
|
720
|
+
) {
|
|
721
|
+
const includePrimitive = (primitive) =>
|
|
722
|
+
!excludedPrimitives.has(primitive) &&
|
|
723
|
+
PcbSvgRenderer.#isDetailPrimitive(primitive, semanticContext)
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
fills: (primitives.fills || []).filter(includePrimitive),
|
|
727
|
+
tracks: (primitives.tracks || []).filter(includePrimitive),
|
|
728
|
+
arcs: (primitives.arcs || []).filter(includePrimitive),
|
|
729
|
+
regions: (primitives.regions || []).filter(includePrimitive)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Builds an identity set for primitives rendered by a specialized path.
|
|
735
|
+
* @param {{ fills?: object[], tracks?: object[], arcs?: object[], regions?: object[] }} primitiveCollections Primitive collections.
|
|
736
|
+
* @returns {Set<object>}
|
|
737
|
+
*/
|
|
738
|
+
static #primitiveIdentitySet(primitiveCollections) {
|
|
739
|
+
return new Set([
|
|
740
|
+
...(primitiveCollections?.fills || []),
|
|
741
|
+
...(primitiveCollections?.tracks || []),
|
|
742
|
+
...(primitiveCollections?.arcs || []),
|
|
743
|
+
...(primitiveCollections?.regions || [])
|
|
744
|
+
])
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Returns true when a primitive should render as generic layer detail.
|
|
749
|
+
* @param {object} primitive Primitive record.
|
|
750
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
751
|
+
* @returns {boolean}
|
|
752
|
+
*/
|
|
753
|
+
static #isDetailPrimitive(primitive, semanticContext) {
|
|
754
|
+
const layer = PcbSvgRenderer.#knownLayerForPrimitive(
|
|
755
|
+
primitive,
|
|
756
|
+
semanticContext
|
|
757
|
+
)
|
|
758
|
+
const role = PcbSvgRenderer.#layerRoleForDetail(layer)
|
|
759
|
+
|
|
760
|
+
return Boolean(layer) && !['copper', 'multi-layer'].includes(role)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Renders non-copper fabrication and documentation primitives.
|
|
765
|
+
* @param {{ fills: object[], tracks: object[], arcs: object[], regions: object[] }} primitives Primitive collections.
|
|
766
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
767
|
+
* @returns {string}
|
|
768
|
+
*/
|
|
769
|
+
static #renderDetailPrimitives(primitives, semanticContext) {
|
|
770
|
+
const fillMarkup = primitives.fills
|
|
771
|
+
.map((fill, index) =>
|
|
772
|
+
PcbSvgRenderer.#renderDetailFill(
|
|
773
|
+
fill,
|
|
774
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
775
|
+
semanticContext,
|
|
776
|
+
'fills',
|
|
777
|
+
fill,
|
|
778
|
+
index
|
|
779
|
+
),
|
|
780
|
+
semanticContext
|
|
781
|
+
)
|
|
782
|
+
)
|
|
783
|
+
.join('')
|
|
784
|
+
const trackMarkup = primitives.tracks
|
|
785
|
+
.map((track, index) =>
|
|
786
|
+
PcbSvgRenderer.#renderDetailTrack(
|
|
787
|
+
track,
|
|
788
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
789
|
+
semanticContext,
|
|
790
|
+
'tracks',
|
|
791
|
+
track,
|
|
792
|
+
index
|
|
793
|
+
),
|
|
794
|
+
semanticContext
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
.join('')
|
|
798
|
+
const arcMarkup = primitives.arcs
|
|
799
|
+
.map((arc, index) =>
|
|
800
|
+
PcbSvgRenderer.#renderDetailArc(
|
|
801
|
+
arc,
|
|
802
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
803
|
+
semanticContext,
|
|
804
|
+
'arcs',
|
|
805
|
+
arc,
|
|
806
|
+
index
|
|
807
|
+
),
|
|
808
|
+
semanticContext
|
|
809
|
+
)
|
|
810
|
+
)
|
|
811
|
+
.join('')
|
|
812
|
+
const regionMarkup = primitives.regions
|
|
813
|
+
.map((region, index) =>
|
|
814
|
+
PcbSvgRenderer.#renderDetailRegion(
|
|
815
|
+
region,
|
|
816
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
817
|
+
semanticContext,
|
|
818
|
+
'regions',
|
|
819
|
+
region,
|
|
820
|
+
index
|
|
821
|
+
),
|
|
822
|
+
semanticContext
|
|
823
|
+
)
|
|
824
|
+
)
|
|
825
|
+
.join('')
|
|
826
|
+
|
|
827
|
+
return fillMarkup || trackMarkup || arcMarkup || regionMarkup
|
|
828
|
+
? '<g class="pcb-detail-layers">' +
|
|
829
|
+
fillMarkup +
|
|
830
|
+
trackMarkup +
|
|
831
|
+
arcMarkup +
|
|
832
|
+
regionMarkup +
|
|
833
|
+
'</g>'
|
|
834
|
+
: ''
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Renders one non-copper fill primitive.
|
|
839
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }} fill Fill primitive.
|
|
840
|
+
* @param {number} index Stable primitive index.
|
|
841
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
842
|
+
* @returns {string}
|
|
843
|
+
*/
|
|
844
|
+
static #renderDetailFill(fill, index, semanticContext) {
|
|
845
|
+
const x = Math.min(fill.x1, fill.x2)
|
|
846
|
+
const y = Math.min(fill.y1, fill.y2)
|
|
847
|
+
const width = Math.abs(fill.x2 - fill.x1)
|
|
848
|
+
const height = Math.abs(fill.y2 - fill.y1)
|
|
849
|
+
const className = PcbSvgRenderer.#detailPrimitiveClass(
|
|
850
|
+
'fill',
|
|
851
|
+
fill,
|
|
852
|
+
semanticContext
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
return (
|
|
856
|
+
'<rect class="' +
|
|
857
|
+
className +
|
|
858
|
+
'" x="' +
|
|
859
|
+
SchematicSvgUtils.formatNumber(x) +
|
|
860
|
+
'" y="' +
|
|
861
|
+
SchematicSvgUtils.formatNumber(y) +
|
|
862
|
+
'" width="' +
|
|
863
|
+
SchematicSvgUtils.formatNumber(width) +
|
|
864
|
+
'" height="' +
|
|
865
|
+
SchematicSvgUtils.formatNumber(height) +
|
|
866
|
+
'" rx="' +
|
|
867
|
+
SchematicSvgUtils.formatNumber(Math.min(width, height) / 6) +
|
|
868
|
+
'"' +
|
|
869
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
870
|
+
'fill',
|
|
871
|
+
fill,
|
|
872
|
+
index,
|
|
873
|
+
semanticContext
|
|
874
|
+
) +
|
|
875
|
+
' />'
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Renders one non-copper track primitive.
|
|
881
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, width?: number }} track Track primitive.
|
|
882
|
+
* @param {number} index Stable primitive index.
|
|
883
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
884
|
+
* @returns {string}
|
|
885
|
+
*/
|
|
886
|
+
static #renderDetailTrack(track, index, semanticContext) {
|
|
887
|
+
const className = PcbSvgRenderer.#detailPrimitiveClass(
|
|
888
|
+
'track',
|
|
889
|
+
track,
|
|
890
|
+
semanticContext
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
return (
|
|
894
|
+
'<line class="' +
|
|
895
|
+
className +
|
|
896
|
+
'" x1="' +
|
|
897
|
+
SchematicSvgUtils.formatNumber(track.x1) +
|
|
898
|
+
'" y1="' +
|
|
899
|
+
SchematicSvgUtils.formatNumber(track.y1) +
|
|
900
|
+
'" x2="' +
|
|
901
|
+
SchematicSvgUtils.formatNumber(track.x2) +
|
|
902
|
+
'" y2="' +
|
|
903
|
+
SchematicSvgUtils.formatNumber(track.y2) +
|
|
904
|
+
'" stroke-width="' +
|
|
905
|
+
SchematicSvgUtils.formatNumber(Math.max(track.width || 0, 1)) +
|
|
906
|
+
'"' +
|
|
907
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
908
|
+
'track',
|
|
909
|
+
track,
|
|
910
|
+
index,
|
|
911
|
+
semanticContext
|
|
912
|
+
) +
|
|
913
|
+
' />'
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Renders one non-copper arc primitive.
|
|
919
|
+
* @param {object} arc Arc primitive.
|
|
920
|
+
* @param {number} index Stable primitive index.
|
|
921
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
922
|
+
* @returns {string}
|
|
923
|
+
*/
|
|
924
|
+
static #renderDetailArc(arc, index, semanticContext) {
|
|
925
|
+
return PcbSvgRenderer.#appendSvgAttributes(
|
|
926
|
+
PcbArcUtils.buildMarkup(
|
|
927
|
+
arc,
|
|
928
|
+
PcbSvgRenderer.#detailPrimitiveClass(
|
|
929
|
+
'arc',
|
|
930
|
+
arc,
|
|
931
|
+
semanticContext
|
|
932
|
+
)
|
|
933
|
+
),
|
|
934
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
935
|
+
'arc',
|
|
936
|
+
arc,
|
|
937
|
+
index,
|
|
938
|
+
semanticContext
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Renders one non-copper region primitive.
|
|
945
|
+
* @param {object} region Region primitive.
|
|
946
|
+
* @param {number} index Stable primitive index.
|
|
947
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
948
|
+
* @returns {string}
|
|
949
|
+
*/
|
|
950
|
+
static #renderDetailRegion(region, index, semanticContext) {
|
|
951
|
+
return PcbSvgRenderer.#appendSvgAttributes(
|
|
952
|
+
PcbRegionPrimitiveRenderer.buildMarkup(
|
|
953
|
+
[region],
|
|
954
|
+
PcbSvgRenderer.#detailPrimitiveClass(
|
|
955
|
+
'region',
|
|
956
|
+
region,
|
|
957
|
+
semanticContext
|
|
958
|
+
)
|
|
959
|
+
),
|
|
960
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
961
|
+
'region',
|
|
962
|
+
region,
|
|
963
|
+
index,
|
|
964
|
+
semanticContext
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Builds one detail primitive CSS class string.
|
|
971
|
+
* @param {string} primitiveKind Primitive kind.
|
|
972
|
+
* @param {object} primitive Primitive record.
|
|
973
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
974
|
+
* @returns {string}
|
|
975
|
+
*/
|
|
976
|
+
static #detailPrimitiveClass(primitiveKind, primitive, semanticContext) {
|
|
977
|
+
const layer = PcbSvgRenderer.#layerForPrimitive(
|
|
978
|
+
primitive,
|
|
979
|
+
semanticContext
|
|
980
|
+
)
|
|
981
|
+
const role = PcbSvgRenderer.#cssToken(
|
|
982
|
+
PcbSvgRenderer.#layerRoleForDetail(layer)
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
return (
|
|
986
|
+
'pcb-detail-' +
|
|
987
|
+
primitiveKind +
|
|
988
|
+
' pcb-detail-' +
|
|
989
|
+
primitiveKind +
|
|
990
|
+
'--' +
|
|
991
|
+
role
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Resolves a layer role suitable for generic detail styling.
|
|
997
|
+
* @param {object | null} layer Layer descriptor.
|
|
998
|
+
* @returns {string}
|
|
999
|
+
*/
|
|
1000
|
+
static #layerRoleForDetail(layer) {
|
|
1001
|
+
return String(layer?.role || 'other')
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Converts arbitrary text to a safe CSS class token.
|
|
1006
|
+
* @param {unknown} value Raw token value.
|
|
1007
|
+
* @returns {string}
|
|
1008
|
+
*/
|
|
1009
|
+
static #cssToken(value) {
|
|
1010
|
+
return (
|
|
1011
|
+
String(value || 'other')
|
|
1012
|
+
.trim()
|
|
1013
|
+
.toLowerCase()
|
|
1014
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
1015
|
+
.replace(/^-+|-+$/gu, '') || 'other'
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
597
1019
|
/**
|
|
598
1020
|
* Normalizes renderer view options.
|
|
599
1021
|
* @param {{ viewKind?: string, layerView?: object } | undefined} options Render options.
|
|
@@ -721,17 +1143,31 @@ export class PcbSvgRenderer {
|
|
|
721
1143
|
primitive?.layerId,
|
|
722
1144
|
primitive?.layerCode
|
|
723
1145
|
])
|
|
724
|
-
|
|
725
|
-
|
|
1146
|
+
const layerIds = PcbSvgRenderer.#layerDescriptorIds(layerView)
|
|
1147
|
+
if (Number.isInteger(layerId) && layerIds.length) {
|
|
1148
|
+
return layerIds.includes(layerId)
|
|
726
1149
|
}
|
|
727
1150
|
|
|
728
1151
|
const primitiveName =
|
|
729
1152
|
primitive?.layerName || primitive?.layer || primitive?.side || ''
|
|
730
1153
|
if (primitiveName && layerView?.displayName) {
|
|
731
|
-
|
|
1154
|
+
if (
|
|
732
1155
|
PcbSvgRenderer.#normalizeSemanticLookup(primitiveName) ===
|
|
733
1156
|
PcbSvgRenderer.#normalizeSemanticLookup(layerView.displayName)
|
|
734
|
-
)
|
|
1157
|
+
) {
|
|
1158
|
+
return true
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const primitiveLayerId =
|
|
1162
|
+
PcbSvgRenderer.#legacyLayerIdForPrimitiveLayerName(
|
|
1163
|
+
primitiveName
|
|
1164
|
+
)
|
|
1165
|
+
if (
|
|
1166
|
+
Number.isInteger(primitiveLayerId) &&
|
|
1167
|
+
layerIds.includes(primitiveLayerId)
|
|
1168
|
+
) {
|
|
1169
|
+
return true
|
|
1170
|
+
}
|
|
735
1171
|
}
|
|
736
1172
|
|
|
737
1173
|
return (
|
|
@@ -768,6 +1204,12 @@ export class PcbSvgRenderer {
|
|
|
768
1204
|
) {
|
|
769
1205
|
layersById.set(descriptor.layerId, descriptor)
|
|
770
1206
|
}
|
|
1207
|
+
if (
|
|
1208
|
+
Number.isInteger(descriptor.legacyLayerId) &&
|
|
1209
|
+
!layersById.has(descriptor.legacyLayerId)
|
|
1210
|
+
) {
|
|
1211
|
+
layersById.set(descriptor.legacyLayerId, descriptor)
|
|
1212
|
+
}
|
|
771
1213
|
const normalizedName = PcbSvgRenderer.#normalizeSemanticLookup(
|
|
772
1214
|
descriptor.displayName
|
|
773
1215
|
)
|
|
@@ -830,6 +1272,7 @@ export class PcbSvgRenderer {
|
|
|
830
1272
|
fills: PcbSvgRenderer.#objectIndexMap(pcb?.fills || []),
|
|
831
1273
|
tracks: PcbSvgRenderer.#objectIndexMap(pcb?.tracks || []),
|
|
832
1274
|
arcs: PcbSvgRenderer.#objectIndexMap(pcb?.arcs || []),
|
|
1275
|
+
regions: PcbSvgRenderer.#objectIndexMap(pcb?.regions || []),
|
|
833
1276
|
vias: PcbSvgRenderer.#objectIndexMap(pcb?.vias || []),
|
|
834
1277
|
pads: PcbSvgRenderer.#objectIndexMap(pcb?.pads || []),
|
|
835
1278
|
texts: PcbSvgRenderer.#objectIndexMap(pcb?.texts || []),
|
|
@@ -884,6 +1327,12 @@ export class PcbSvgRenderer {
|
|
|
884
1327
|
pcb?.arcs || [],
|
|
885
1328
|
semanticContext
|
|
886
1329
|
),
|
|
1330
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
1331
|
+
'region',
|
|
1332
|
+
'regions',
|
|
1333
|
+
pcb?.regions || [],
|
|
1334
|
+
semanticContext
|
|
1335
|
+
),
|
|
887
1336
|
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
888
1337
|
'via',
|
|
889
1338
|
'vias',
|
|
@@ -1370,6 +1819,7 @@ export class PcbSvgRenderer {
|
|
|
1370
1819
|
layer.id,
|
|
1371
1820
|
layer.index
|
|
1372
1821
|
])
|
|
1822
|
+
const legacyLayerId = PcbSvgRenderer.#legacyLayerId(layer, layerId)
|
|
1373
1823
|
const displayName =
|
|
1374
1824
|
layer.displayName || layer.name || layer.layerName || ''
|
|
1375
1825
|
const layerKey = Number.isInteger(layerId)
|
|
@@ -1382,12 +1832,16 @@ export class PcbSvgRenderer {
|
|
|
1382
1832
|
|
|
1383
1833
|
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1384
1834
|
layerId,
|
|
1835
|
+
legacyLayerId,
|
|
1385
1836
|
layerKey,
|
|
1386
1837
|
displayName: displayName || layerKey,
|
|
1387
1838
|
role:
|
|
1388
1839
|
layer.role ||
|
|
1389
1840
|
layer.layerRole ||
|
|
1390
|
-
PcbSvgRenderer.#inferLayerRole(
|
|
1841
|
+
PcbSvgRenderer.#inferLayerRole(
|
|
1842
|
+
displayName,
|
|
1843
|
+
legacyLayerId ?? layerId
|
|
1844
|
+
)
|
|
1391
1845
|
})
|
|
1392
1846
|
}
|
|
1393
1847
|
|
|
@@ -1398,10 +1852,50 @@ export class PcbSvgRenderer {
|
|
|
1398
1852
|
*/
|
|
1399
1853
|
static #includedLayerIds(semanticContext) {
|
|
1400
1854
|
return (semanticContext?.layerDescriptors || [])
|
|
1401
|
-
.
|
|
1855
|
+
.flatMap((layer) => PcbSvgRenderer.#layerDescriptorIds(layer))
|
|
1402
1856
|
.filter((layerId) => Number.isInteger(layerId))
|
|
1403
1857
|
}
|
|
1404
1858
|
|
|
1859
|
+
/**
|
|
1860
|
+
* Returns all numeric identifiers that can address one layer descriptor.
|
|
1861
|
+
* @param {object | null} layer Layer descriptor.
|
|
1862
|
+
* @returns {number[]}
|
|
1863
|
+
*/
|
|
1864
|
+
static #layerDescriptorIds(layer) {
|
|
1865
|
+
return [
|
|
1866
|
+
PcbSvgRenderer.#firstFiniteNumber([layer?.layerId]),
|
|
1867
|
+
PcbSvgRenderer.#firstFiniteNumber([layer?.legacyLayerId])
|
|
1868
|
+
].filter(
|
|
1869
|
+
(layerId, index, layerIds) =>
|
|
1870
|
+
Number.isInteger(layerId) && layerIds.indexOf(layerId) === index
|
|
1871
|
+
)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
/**
|
|
1875
|
+
* Resolves a legacy primitive-layer alias for a layer descriptor.
|
|
1876
|
+
* @param {object} layer Layer record.
|
|
1877
|
+
* @param {number | undefined} layerId Primary layer id.
|
|
1878
|
+
* @returns {number | undefined}
|
|
1879
|
+
*/
|
|
1880
|
+
static #legacyLayerId(layer, layerId) {
|
|
1881
|
+
const explicitLegacyLayerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1882
|
+
layer.legacyLayerId,
|
|
1883
|
+
layer.legacyId
|
|
1884
|
+
])
|
|
1885
|
+
const legacyLayerId = Number.isInteger(explicitLegacyLayerId)
|
|
1886
|
+
? explicitLegacyLayerId
|
|
1887
|
+
: PcbLayerIdCodec.legacyLayerIdFromV7SaveId(layerId)
|
|
1888
|
+
|
|
1889
|
+
if (
|
|
1890
|
+
!Number.isInteger(legacyLayerId) ||
|
|
1891
|
+
(Number.isInteger(layerId) && legacyLayerId === layerId)
|
|
1892
|
+
) {
|
|
1893
|
+
return undefined
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
return legacyLayerId
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1405
1899
|
/**
|
|
1406
1900
|
* Returns true when a PCB view contains only board outline metadata.
|
|
1407
1901
|
* @param {object} pcb Normalized PCB model.
|
|
@@ -1426,9 +1920,15 @@ export class PcbSvgRenderer {
|
|
|
1426
1920
|
/**
|
|
1427
1921
|
* Infers one broad rendering role from a layer name.
|
|
1428
1922
|
* @param {string} displayName Layer display name.
|
|
1923
|
+
* @param {number | undefined} layerId Legacy layer id.
|
|
1429
1924
|
* @returns {string}
|
|
1430
1925
|
*/
|
|
1431
|
-
static #inferLayerRole(displayName) {
|
|
1926
|
+
static #inferLayerRole(displayName, layerId) {
|
|
1927
|
+
const groupedRole = PcbSvgRenderer.#inferLayerRoleFromId(layerId)
|
|
1928
|
+
if (groupedRole) {
|
|
1929
|
+
return groupedRole
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1432
1932
|
const normalized = String(displayName || '').toLowerCase()
|
|
1433
1933
|
if (/overlay|silk/u.test(normalized)) return 'overlay'
|
|
1434
1934
|
if (/paste/u.test(normalized)) return 'paste'
|
|
@@ -1437,10 +1937,42 @@ export class PcbSvgRenderer {
|
|
|
1437
1937
|
return 'mechanical'
|
|
1438
1938
|
}
|
|
1439
1939
|
if (/drill/u.test(normalized)) return 'drill'
|
|
1440
|
-
if (
|
|
1940
|
+
if (
|
|
1941
|
+
/layer|copper|plane/u.test(normalized) ||
|
|
1942
|
+
/\binternal[-_\s]*\d+\b/u.test(normalized) ||
|
|
1943
|
+
/\binner[-_\s]*\d+\b/u.test(normalized)
|
|
1944
|
+
) {
|
|
1945
|
+
return 'copper'
|
|
1946
|
+
}
|
|
1441
1947
|
return 'other'
|
|
1442
1948
|
}
|
|
1443
1949
|
|
|
1950
|
+
/**
|
|
1951
|
+
* Infers one layer role from a legacy Altium layer id.
|
|
1952
|
+
* @param {unknown} layerId Legacy layer id.
|
|
1953
|
+
* @returns {string}
|
|
1954
|
+
*/
|
|
1955
|
+
static #inferLayerRoleFromId(layerId) {
|
|
1956
|
+
const legacyLayerId = Number(layerId)
|
|
1957
|
+
if (!Number.isInteger(legacyLayerId)) {
|
|
1958
|
+
return ''
|
|
1959
|
+
}
|
|
1960
|
+
if (
|
|
1961
|
+
PcbLayerGroups.isCopper(legacyLayerId) ||
|
|
1962
|
+
PcbLayerGroups.isInternalPlane(legacyLayerId)
|
|
1963
|
+
) {
|
|
1964
|
+
return 'copper'
|
|
1965
|
+
}
|
|
1966
|
+
if (PcbLayerGroups.isOverlay(legacyLayerId)) return 'overlay'
|
|
1967
|
+
if (PcbLayerGroups.isPaste(legacyLayerId)) return 'paste'
|
|
1968
|
+
if (PcbLayerGroups.isSolderMask(legacyLayerId)) return 'mask'
|
|
1969
|
+
if (PcbLayerGroups.isDrill(legacyLayerId)) return 'drill'
|
|
1970
|
+
if (PcbLayerGroups.isKeepout(legacyLayerId)) return 'keepout'
|
|
1971
|
+
if (PcbLayerGroups.isMechanical(legacyLayerId)) return 'mechanical'
|
|
1972
|
+
if (PcbLayerGroups.isMultiLayer(legacyLayerId)) return 'multi-layer'
|
|
1973
|
+
return ''
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1444
1976
|
/**
|
|
1445
1977
|
* Resolves drill plating metadata for SVG and sidecar output.
|
|
1446
1978
|
* @param {object} primitive Drill owner primitive.
|
|
@@ -1504,21 +2036,46 @@ export class PcbSvgRenderer {
|
|
|
1504
2036
|
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
1505
2037
|
*/
|
|
1506
2038
|
static #layerForPrimitive(primitive, semanticContext) {
|
|
2039
|
+
const knownLayer = PcbSvgRenderer.#knownLayerForPrimitive(
|
|
2040
|
+
primitive,
|
|
2041
|
+
semanticContext
|
|
2042
|
+
)
|
|
2043
|
+
if (knownLayer) {
|
|
2044
|
+
return knownLayer
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1507
2047
|
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1508
2048
|
primitive?.layerId,
|
|
1509
2049
|
primitive?.layerCode
|
|
1510
2050
|
])
|
|
1511
2051
|
if (Number.isInteger(layerId)) {
|
|
1512
|
-
return
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
2052
|
+
return {
|
|
2053
|
+
layerId,
|
|
2054
|
+
layerKey: 'L' + layerId,
|
|
2055
|
+
displayName:
|
|
2056
|
+
primitive?.layerName ||
|
|
2057
|
+
primitive?.layer ||
|
|
2058
|
+
'Layer ' + layerId,
|
|
2059
|
+
role: PcbSvgRenderer.#inferLayerRole('', layerId)
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
return null
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* Resolves a layer descriptor only when the layer is in recovered metadata.
|
|
2068
|
+
* @param {object} primitive Primitive record.
|
|
2069
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
2070
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
2071
|
+
*/
|
|
2072
|
+
static #knownLayerForPrimitive(primitive, semanticContext) {
|
|
2073
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
2074
|
+
primitive?.layerId,
|
|
2075
|
+
primitive?.layerCode
|
|
2076
|
+
])
|
|
2077
|
+
if (Number.isInteger(layerId)) {
|
|
2078
|
+
return semanticContext.layersById.get(layerId) || null
|
|
1522
2079
|
}
|
|
1523
2080
|
|
|
1524
2081
|
const layerName =
|
|
@@ -1527,7 +2084,63 @@ export class PcbSvgRenderer {
|
|
|
1527
2084
|
PcbSvgRenderer.#normalizeSemanticLookup(layerName)
|
|
1528
2085
|
)
|
|
1529
2086
|
|
|
1530
|
-
return
|
|
2087
|
+
return (
|
|
2088
|
+
byName ||
|
|
2089
|
+
PcbSvgRenderer.#layerForPrimitiveLayerAlias(
|
|
2090
|
+
layerName,
|
|
2091
|
+
semanticContext
|
|
2092
|
+
)
|
|
2093
|
+
)
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* Resolves legacy Altium primitive layer names such as MID2 to a layer.
|
|
2098
|
+
* @param {string} layerName Primitive layer name.
|
|
2099
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
2100
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
2101
|
+
*/
|
|
2102
|
+
static #layerForPrimitiveLayerAlias(layerName, semanticContext) {
|
|
2103
|
+
const legacyLayerId =
|
|
2104
|
+
PcbSvgRenderer.#legacyLayerIdForPrimitiveLayerName(layerName)
|
|
2105
|
+
|
|
2106
|
+
return Number.isInteger(legacyLayerId)
|
|
2107
|
+
? semanticContext.layersById.get(legacyLayerId) || null
|
|
2108
|
+
: null
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* Converts common primitive layer aliases into legacy layer ids.
|
|
2113
|
+
* @param {string} layerName Primitive layer name.
|
|
2114
|
+
* @returns {number | undefined}
|
|
2115
|
+
*/
|
|
2116
|
+
static #legacyLayerIdForPrimitiveLayerName(layerName) {
|
|
2117
|
+
const compact = PcbSvgRenderer.#normalizeSemanticLookup(
|
|
2118
|
+
layerName
|
|
2119
|
+
).replace(/[\s_-]+/gu, '')
|
|
2120
|
+
|
|
2121
|
+
if (compact === 'TOP' || compact === 'TOPLAYER') {
|
|
2122
|
+
return 1
|
|
2123
|
+
}
|
|
2124
|
+
if (
|
|
2125
|
+
compact === 'BOTTOM' ||
|
|
2126
|
+
compact === 'BOTTOMLAYER' ||
|
|
2127
|
+
compact === 'BOT' ||
|
|
2128
|
+
compact === 'BOTLAYER'
|
|
2129
|
+
) {
|
|
2130
|
+
return 32
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
const midLayerMatch = compact.match(/^MID(?:DLE)?(?:LAYER)?(\d+)$/u)
|
|
2134
|
+
if (midLayerMatch) {
|
|
2135
|
+
return Number(midLayerMatch[1]) + 1
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const internalLayerMatch = compact.match(/^(?:INTERNAL|INNER)(\d+)$/u)
|
|
2139
|
+
if (internalLayerMatch) {
|
|
2140
|
+
return Number(internalLayerMatch[1]) + 1
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
return undefined
|
|
1531
2144
|
}
|
|
1532
2145
|
|
|
1533
2146
|
/**
|
|
@@ -1996,6 +2609,29 @@ export class PcbSvgRenderer {
|
|
|
1996
2609
|
)
|
|
1997
2610
|
}
|
|
1998
2611
|
|
|
2612
|
+
/**
|
|
2613
|
+
* Renders semantic attributes for a generated pad mask aperture.
|
|
2614
|
+
* @param {object} aperture Pad mask aperture descriptor.
|
|
2615
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
2616
|
+
* @returns {string}
|
|
2617
|
+
*/
|
|
2618
|
+
static #padMaskApertureAttributes(aperture, semanticContext) {
|
|
2619
|
+
return (
|
|
2620
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
2621
|
+
aperture.spec.primitiveKind,
|
|
2622
|
+
aperture.primitive,
|
|
2623
|
+
aperture.index,
|
|
2624
|
+
semanticContext
|
|
2625
|
+
) +
|
|
2626
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
2627
|
+
'data-pad-number': PcbSvgRenderer.#padNumber(aperture.pad),
|
|
2628
|
+
'data-mask-kind': aperture.spec.kind,
|
|
2629
|
+
'data-mask-side': aperture.spec.side,
|
|
2630
|
+
'data-source-pad-element-key': 'pcb-pad-' + aperture.padIndex
|
|
2631
|
+
})
|
|
2632
|
+
)
|
|
2633
|
+
}
|
|
2634
|
+
|
|
1999
2635
|
/**
|
|
2000
2636
|
* Renders one pad drill hole as SVG.
|
|
2001
2637
|
* @param {{ holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null }} pad
|