altium-toolkit 1.4.3 → 1.4.5
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
CHANGED
|
@@ -13,11 +13,13 @@ export class PcbSideResolvedRenderModel {
|
|
|
13
13
|
/**
|
|
14
14
|
* Resolves a normalized PCB model for the requested board side.
|
|
15
15
|
* @param {object | null} board
|
|
16
|
-
* @param {'front' | 'back' | { side?: 'front' | 'back' }} [options]
|
|
16
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} [options]
|
|
17
17
|
* @returns {object | null}
|
|
18
18
|
*/
|
|
19
19
|
static resolve(board, options = {}) {
|
|
20
20
|
const side = PcbSideResolvedRenderModel.#normalizeSide(options)
|
|
21
|
+
const includeOppositeCopper =
|
|
22
|
+
PcbSideResolvedRenderModel.#includeOppositeCopper(options)
|
|
21
23
|
const resolved = HistoricalPcbSideResolvedRenderModel.resolve(
|
|
22
24
|
board,
|
|
23
25
|
options
|
|
@@ -52,7 +54,13 @@ export class PcbSideResolvedRenderModel {
|
|
|
52
54
|
boardRegions: PcbSideResolvedRenderModel.#filterPrimitives(
|
|
53
55
|
pcb.boardRegions,
|
|
54
56
|
side
|
|
55
|
-
)
|
|
57
|
+
),
|
|
58
|
+
pads: includeOppositeCopper
|
|
59
|
+
? PcbSideResolvedRenderModel.#prepareContextPads(
|
|
60
|
+
board?.pcb?.pads,
|
|
61
|
+
side
|
|
62
|
+
)
|
|
63
|
+
: pcb.pads
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
}
|
|
@@ -68,7 +76,7 @@ export class PcbSideResolvedRenderModel {
|
|
|
68
76
|
|
|
69
77
|
/**
|
|
70
78
|
* Normalizes the caller side option.
|
|
71
|
-
* @param {'front' | 'back' | { side?: 'front' | 'back' }} options
|
|
79
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} options
|
|
72
80
|
* @returns {'front' | 'back'}
|
|
73
81
|
*/
|
|
74
82
|
static #normalizeSide(options) {
|
|
@@ -79,6 +87,106 @@ export class PcbSideResolvedRenderModel {
|
|
|
79
87
|
return 'front'
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Checks whether the caller requested opposite-side copper context.
|
|
92
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} options
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
static #includeOppositeCopper(options) {
|
|
96
|
+
return Boolean(
|
|
97
|
+
options &&
|
|
98
|
+
typeof options === 'object' &&
|
|
99
|
+
options.includeOppositeCopper === true
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Projects every copper-bearing pad into the requested composite view.
|
|
105
|
+
* @param {readonly object[] | undefined} pads Source pads.
|
|
106
|
+
* @param {'front' | 'back'} side Requested board side.
|
|
107
|
+
* @returns {object[]}
|
|
108
|
+
*/
|
|
109
|
+
static #prepareContextPads(pads, side) {
|
|
110
|
+
return (pads || []).map((pad) =>
|
|
111
|
+
PcbSideResolvedRenderModel.#projectPadForTopRenderer(pad, side)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Projects the authored aperture for one pad into the top-oriented
|
|
117
|
+
* renderer while retaining front-only apertures as opposite-side context.
|
|
118
|
+
* @param {object} pad Source pad.
|
|
119
|
+
* @param {'front' | 'back'} side Requested board side.
|
|
120
|
+
* @returns {object}
|
|
121
|
+
*/
|
|
122
|
+
static #projectPadForTopRenderer(pad, side) {
|
|
123
|
+
const layerId = PcbSideResolvedRenderModel.#effectivePadLayerId(pad)
|
|
124
|
+
const apertureSide =
|
|
125
|
+
layerId === 1 ? 'front' : layerId === 32 ? 'back' : side
|
|
126
|
+
const projectedPad = {
|
|
127
|
+
...pad,
|
|
128
|
+
copperRenderGroup: apertureSide === side ? 'surface' : 'subsurface'
|
|
129
|
+
}
|
|
130
|
+
if (apertureSide !== 'back') return projectedPad
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...projectedPad,
|
|
134
|
+
sizeTopX: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
135
|
+
pad.sizeBottomX,
|
|
136
|
+
pad.sizeMidX,
|
|
137
|
+
pad.sizeTopX
|
|
138
|
+
),
|
|
139
|
+
sizeTopY: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
140
|
+
pad.sizeBottomY,
|
|
141
|
+
pad.sizeMidY,
|
|
142
|
+
pad.sizeTopY
|
|
143
|
+
),
|
|
144
|
+
shapeTop: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
145
|
+
pad.shapeBottom,
|
|
146
|
+
pad.shapeMid,
|
|
147
|
+
pad.shapeTop
|
|
148
|
+
),
|
|
149
|
+
roundedRectShapeTop: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
150
|
+
pad.roundedRectShapeBottom,
|
|
151
|
+
pad.roundedRectShapeMid,
|
|
152
|
+
pad.roundedRectShapeTop
|
|
153
|
+
),
|
|
154
|
+
cornerRadiusTop: PcbSideResolvedRenderModel.#firstFiniteValue(
|
|
155
|
+
pad.cornerRadiusBottom,
|
|
156
|
+
pad.cornerRadiusMid,
|
|
157
|
+
pad.cornerRadiusTop
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolves the authored Altium layer id for a pad.
|
|
164
|
+
* @param {object | null} pad Pad to inspect.
|
|
165
|
+
* @returns {number | null}
|
|
166
|
+
*/
|
|
167
|
+
static #effectivePadLayerId(pad) {
|
|
168
|
+
const layerId = Number(pad?.layerId)
|
|
169
|
+
if (Number.isInteger(layerId) && layerId > 0) return layerId
|
|
170
|
+
|
|
171
|
+
const legacyLayerId = Number(pad?.legacyLayerId)
|
|
172
|
+
return Number.isInteger(legacyLayerId) && legacyLayerId > 0
|
|
173
|
+
? legacyLayerId
|
|
174
|
+
: null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns the first finite numeric value.
|
|
179
|
+
* @param {...unknown} values Values to inspect.
|
|
180
|
+
* @returns {number | undefined}
|
|
181
|
+
*/
|
|
182
|
+
static #firstFiniteValue(...values) {
|
|
183
|
+
for (const value of values) {
|
|
184
|
+
const number = Number(value)
|
|
185
|
+
if (Number.isFinite(number)) return number
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
82
190
|
/**
|
|
83
191
|
* Filters one primitive collection to the requested surface.
|
|
84
192
|
* @param {readonly object[] | undefined} primitives
|
|
@@ -115,7 +223,7 @@ export class PcbSideResolvedRenderModel {
|
|
|
115
223
|
/**
|
|
116
224
|
* Resolves a normalized PCB model for the requested board side.
|
|
117
225
|
* @param {object | null} board
|
|
118
|
-
* @param {'front' | 'back' | { side?: 'front' | 'back' }} [options]
|
|
226
|
+
* @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} [options]
|
|
119
227
|
* @returns {object | null}
|
|
120
228
|
*/
|
|
121
229
|
export function preparePcbSideResolvedRenderModel(board, options = {}) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { PcbSvgRenderer as LegacyPcbSvgRenderer } from '../ui/PcbSvgRenderer.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders native Altium PCB models through the preserved historical renderer
|
|
8
|
+
* while applying convergence-owned copper grouping semantics.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbSvgRenderer {
|
|
11
|
+
static #SUBSURFACE_GROUP = '<g class="pcb-copper pcb-copper--subsurface">'
|
|
12
|
+
static #SURFACE_GROUP = '<g class="pcb-copper pcb-copper--surface">'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders one native Altium PCB document as SVG markup.
|
|
16
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
17
|
+
* @param {Record<string, any>} [options] Historical renderer options.
|
|
18
|
+
* @returns {string} Rendered SVG panel markup.
|
|
19
|
+
*/
|
|
20
|
+
static render(documentModel, options = {}) {
|
|
21
|
+
const markup = LegacyPcbSvgRenderer.render(documentModel, options)
|
|
22
|
+
const subsurfacePadIndexes = PcbSvgRenderer.#subsurfacePadIndexes(
|
|
23
|
+
documentModel,
|
|
24
|
+
options
|
|
25
|
+
)
|
|
26
|
+
if (subsurfacePadIndexes.length === 0) return markup
|
|
27
|
+
|
|
28
|
+
return PcbSvgRenderer.#movePadsToSubsurfaceGroup(
|
|
29
|
+
markup,
|
|
30
|
+
subsurfacePadIndexes
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Renders one deterministic SVG entry per physical or primitive layer.
|
|
36
|
+
* Layer exports intentionally preserve the historical layer-only output.
|
|
37
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
38
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string, role: string, svg: string }[]}
|
|
39
|
+
*/
|
|
40
|
+
static renderLayerSvgs(documentModel) {
|
|
41
|
+
return LegacyPcbSvgRenderer.renderLayerSvgs(documentModel)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Finds pad indexes whose copper must share the contextual copper group.
|
|
46
|
+
* Layer-only exports do not use composite surface/subsurface grouping.
|
|
47
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
48
|
+
* @param {Record<string, any>} options Historical renderer options.
|
|
49
|
+
* @returns {number[]} Stable pad indexes.
|
|
50
|
+
*/
|
|
51
|
+
static #subsurfacePadIndexes(documentModel, options) {
|
|
52
|
+
if (options?.layerView) return []
|
|
53
|
+
|
|
54
|
+
return (documentModel?.pcb?.pads || [])
|
|
55
|
+
.map((pad, index) =>
|
|
56
|
+
pad?.copperRenderGroup === 'subsurface' ? index : -1
|
|
57
|
+
)
|
|
58
|
+
.filter((index) => index >= 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Moves contextual pad groups into the same SVG group as contextual traces
|
|
63
|
+
* so the browser composites both primitives with one shared opacity.
|
|
64
|
+
* @param {string} markup Historical renderer markup.
|
|
65
|
+
* @param {number[]} padIndexes Stable pad indexes to relocate.
|
|
66
|
+
* @returns {string} Copper-group-aware markup.
|
|
67
|
+
*/
|
|
68
|
+
static #movePadsToSubsurfaceGroup(markup, padIndexes) {
|
|
69
|
+
const subsurfaceStart = markup.indexOf(PcbSvgRenderer.#SUBSURFACE_GROUP)
|
|
70
|
+
const surfaceStart = markup.indexOf(PcbSvgRenderer.#SURFACE_GROUP)
|
|
71
|
+
if (subsurfaceStart < 0 || surfaceStart <= subsurfaceStart) {
|
|
72
|
+
return markup
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const subsurfaceClose = markup.lastIndexOf('</g>', surfaceStart)
|
|
76
|
+
if (subsurfaceClose < subsurfaceStart) return markup
|
|
77
|
+
|
|
78
|
+
let remainingMarkup = markup
|
|
79
|
+
const relocatedPads = []
|
|
80
|
+
for (const padIndex of padIndexes) {
|
|
81
|
+
const result = PcbSvgRenderer.#extractPadGroup(
|
|
82
|
+
remainingMarkup,
|
|
83
|
+
padIndex,
|
|
84
|
+
surfaceStart
|
|
85
|
+
)
|
|
86
|
+
if (!result) continue
|
|
87
|
+
remainingMarkup = result.markup
|
|
88
|
+
relocatedPads.push(result.padMarkup)
|
|
89
|
+
}
|
|
90
|
+
if (relocatedPads.length === 0) return markup
|
|
91
|
+
|
|
92
|
+
const updatedSurfaceStart = remainingMarkup.indexOf(
|
|
93
|
+
PcbSvgRenderer.#SURFACE_GROUP
|
|
94
|
+
)
|
|
95
|
+
const updatedSubsurfaceClose = remainingMarkup.lastIndexOf(
|
|
96
|
+
'</g>',
|
|
97
|
+
updatedSurfaceStart
|
|
98
|
+
)
|
|
99
|
+
return (
|
|
100
|
+
remainingMarkup.slice(0, updatedSubsurfaceClose) +
|
|
101
|
+
relocatedPads.join('') +
|
|
102
|
+
remainingMarkup.slice(updatedSubsurfaceClose)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extracts one top-level pad group from the historical surface group.
|
|
108
|
+
* Pad groups contain only leaf SVG shapes, so their first closing group is
|
|
109
|
+
* also the matching closing tag.
|
|
110
|
+
* @param {string} markup Current renderer markup.
|
|
111
|
+
* @param {number} padIndex Stable pad index.
|
|
112
|
+
* @param {number} minimumStart Earliest valid surface-group position.
|
|
113
|
+
* @returns {{ markup: string, padMarkup: string } | null} Extraction result.
|
|
114
|
+
*/
|
|
115
|
+
static #extractPadGroup(markup, padIndex, minimumStart) {
|
|
116
|
+
const elementKey = 'data-element-key="pcb-pad-' + padIndex + '"'
|
|
117
|
+
const keyStart = markup.indexOf(elementKey, minimumStart)
|
|
118
|
+
if (keyStart < 0) return null
|
|
119
|
+
|
|
120
|
+
const groupStart = markup.lastIndexOf('<g', keyStart)
|
|
121
|
+
const groupEndStart = markup.indexOf('</g>', keyStart)
|
|
122
|
+
if (groupStart < minimumStart || groupEndStart < 0) return null
|
|
123
|
+
|
|
124
|
+
const groupEnd = groupEndStart + '</g>'.length
|
|
125
|
+
return {
|
|
126
|
+
markup: markup.slice(0, groupStart) + markup.slice(groupEnd),
|
|
127
|
+
padMarkup: markup.slice(groupStart, groupEnd)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/extensions.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export * from 'circuitjson-toolkit/extensions'
|
|
5
5
|
|
|
6
6
|
export { AltiumExtensionResolver } from './convergence/AltiumExtensionResolver.mjs'
|
|
7
|
+
export { PcbSvgRenderer } from './convergence/PcbSvgRenderer.mjs'
|
|
7
8
|
export { SchematicSvgRenderer } from './convergence/SchematicSvgRenderer.mjs'
|
|
8
9
|
export * from './legacy-parser.mjs'
|
|
9
10
|
export * from './legacy-netlist-query.mjs'
|