altium-toolkit 1.4.2 → 1.4.4

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.
@@ -0,0 +1,20 @@
1
+ # altium-toolkit 1.4.3
2
+
3
+ Version 1.4.3 keeps PCB fabrication details on their authored board side when
4
+ preparing side-resolved render models.
5
+
6
+ ## Side-correct PCB fabrication details
7
+
8
+ - Front-side views retain top overlay, paste, and solder-mask primitives while
9
+ excluding their bottom-side counterparts.
10
+ - Back-side views retain bottom overlay, paste, and solder-mask primitives while
11
+ excluding their top-side counterparts.
12
+ - Shared mechanical and documentation layers remain visible on both sides.
13
+ - Copper projection continues to use the established native Altium behavior.
14
+
15
+ ## Verification
16
+
17
+ - A side-resolution regression covers fills, tracks, arcs, regions,
18
+ shape-based regions, and board regions with neutral-layer preservation.
19
+ - The complete package suite, formatting, and npm dry-run gates verify the
20
+ release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -0,0 +1,236 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbLayerGroups } from '../core/altium/PcbLayerGroups.mjs'
6
+ import { PcbSideResolvedRenderModel as HistoricalPcbSideResolvedRenderModel } from '../ui/PcbSideResolvedRenderModel.mjs'
7
+
8
+ /**
9
+ * Adds side-correct fabrication detail filtering around the preserved native
10
+ * Altium side projection.
11
+ */
12
+ export class PcbSideResolvedRenderModel {
13
+ /**
14
+ * Resolves a normalized PCB model for the requested board side.
15
+ * @param {object | null} board
16
+ * @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} [options]
17
+ * @returns {object | null}
18
+ */
19
+ static resolve(board, options = {}) {
20
+ const side = PcbSideResolvedRenderModel.#normalizeSide(options)
21
+ const includeOppositeCopper =
22
+ PcbSideResolvedRenderModel.#includeOppositeCopper(options)
23
+ const resolved = HistoricalPcbSideResolvedRenderModel.resolve(
24
+ board,
25
+ options
26
+ )
27
+ if (!resolved?.pcb) return resolved
28
+
29
+ const pcb = resolved.pcb
30
+ return {
31
+ ...resolved,
32
+ pcb: {
33
+ ...pcb,
34
+ fills: PcbSideResolvedRenderModel.#filterPrimitives(
35
+ pcb.fills,
36
+ side
37
+ ),
38
+ tracks: PcbSideResolvedRenderModel.#filterPrimitives(
39
+ pcb.tracks,
40
+ side
41
+ ),
42
+ arcs: PcbSideResolvedRenderModel.#filterPrimitives(
43
+ pcb.arcs,
44
+ side
45
+ ),
46
+ regions: PcbSideResolvedRenderModel.#filterPrimitives(
47
+ pcb.regions,
48
+ side
49
+ ),
50
+ shapeBasedRegions: PcbSideResolvedRenderModel.#filterPrimitives(
51
+ pcb.shapeBasedRegions,
52
+ side
53
+ ),
54
+ boardRegions: PcbSideResolvedRenderModel.#filterPrimitives(
55
+ pcb.boardRegions,
56
+ side
57
+ ),
58
+ pads: includeOppositeCopper
59
+ ? PcbSideResolvedRenderModel.#prepareContextPads(
60
+ board?.pcb?.pads,
61
+ side
62
+ )
63
+ : pcb.pads
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Checks whether a primitive belongs to an Altium copper signal layer.
70
+ * @param {object | null} primitive
71
+ * @returns {boolean}
72
+ */
73
+ static isCopperPrimitive(primitive) {
74
+ return HistoricalPcbSideResolvedRenderModel.isCopperPrimitive(primitive)
75
+ }
76
+
77
+ /**
78
+ * Normalizes the caller side option.
79
+ * @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} options
80
+ * @returns {'front' | 'back'}
81
+ */
82
+ static #normalizeSide(options) {
83
+ if (options === 'back') return 'back'
84
+ if (options && typeof options === 'object' && options.side === 'back') {
85
+ return 'back'
86
+ }
87
+ return 'front'
88
+ }
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
+ if (apertureSide !== 'back') return { ...pad }
127
+
128
+ return {
129
+ ...pad,
130
+ sizeTopX: PcbSideResolvedRenderModel.#firstFiniteValue(
131
+ pad.sizeBottomX,
132
+ pad.sizeMidX,
133
+ pad.sizeTopX
134
+ ),
135
+ sizeTopY: PcbSideResolvedRenderModel.#firstFiniteValue(
136
+ pad.sizeBottomY,
137
+ pad.sizeMidY,
138
+ pad.sizeTopY
139
+ ),
140
+ shapeTop: PcbSideResolvedRenderModel.#firstFiniteValue(
141
+ pad.shapeBottom,
142
+ pad.shapeMid,
143
+ pad.shapeTop
144
+ ),
145
+ roundedRectShapeTop: PcbSideResolvedRenderModel.#firstFiniteValue(
146
+ pad.roundedRectShapeBottom,
147
+ pad.roundedRectShapeMid,
148
+ pad.roundedRectShapeTop
149
+ ),
150
+ cornerRadiusTop: PcbSideResolvedRenderModel.#firstFiniteValue(
151
+ pad.cornerRadiusBottom,
152
+ pad.cornerRadiusMid,
153
+ pad.cornerRadiusTop
154
+ )
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Resolves the authored Altium layer id for a pad.
160
+ * @param {object | null} pad Pad to inspect.
161
+ * @returns {number | null}
162
+ */
163
+ static #effectivePadLayerId(pad) {
164
+ const layerId = Number(pad?.layerId)
165
+ if (Number.isInteger(layerId) && layerId > 0) return layerId
166
+
167
+ const legacyLayerId = Number(pad?.legacyLayerId)
168
+ return Number.isInteger(legacyLayerId) && legacyLayerId > 0
169
+ ? legacyLayerId
170
+ : null
171
+ }
172
+
173
+ /**
174
+ * Returns the first finite numeric value.
175
+ * @param {...unknown} values Values to inspect.
176
+ * @returns {number | undefined}
177
+ */
178
+ static #firstFiniteValue(...values) {
179
+ for (const value of values) {
180
+ const number = Number(value)
181
+ if (Number.isFinite(number)) return number
182
+ }
183
+ return undefined
184
+ }
185
+
186
+ /**
187
+ * Filters one primitive collection to the requested surface.
188
+ * @param {readonly object[] | undefined} primitives
189
+ * @param {'front' | 'back'} side
190
+ * @returns {object[]}
191
+ */
192
+ static #filterPrimitives(primitives, side) {
193
+ return (primitives || []).filter((primitive) =>
194
+ PcbSideResolvedRenderModel.#isPrimitiveVisibleOnSide(
195
+ primitive,
196
+ side
197
+ )
198
+ )
199
+ }
200
+
201
+ /**
202
+ * Checks whether a primitive belongs to the requested surface or a shared
203
+ * non-surface layer.
204
+ * @param {object | null} primitive
205
+ * @param {'front' | 'back'} side
206
+ * @returns {boolean}
207
+ */
208
+ static #isPrimitiveVisibleOnSide(primitive, side) {
209
+ const layerId = primitive?.layerId ?? primitive?.layerCode
210
+ if (PcbLayerGroups.isCopper(layerId)) return true
211
+
212
+ const layerSide = PcbLayerGroups.describeLayer(layerId).side
213
+ if (layerSide === 'top') return side === 'front'
214
+ if (layerSide === 'bottom') return side === 'back'
215
+ return true
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Resolves a normalized PCB model for the requested board side.
221
+ * @param {object | null} board
222
+ * @param {'front' | 'back' | { side?: 'front' | 'back', includeOppositeCopper?: boolean }} [options]
223
+ * @returns {object | null}
224
+ */
225
+ export function preparePcbSideResolvedRenderModel(board, options = {}) {
226
+ return PcbSideResolvedRenderModel.resolve(board, options)
227
+ }
228
+
229
+ /**
230
+ * Checks whether a primitive belongs to an Altium copper signal layer.
231
+ * @param {object | null} primitive
232
+ * @returns {boolean}
233
+ */
234
+ export function isCopperPrimitive(primitive) {
235
+ return PcbSideResolvedRenderModel.isCopperPrimitive(primitive)
236
+ }
@@ -15,7 +15,7 @@ export {
15
15
  PcbSideResolvedRenderModel,
16
16
  isCopperPrimitive,
17
17
  preparePcbSideResolvedRenderModel
18
- } from './ui/PcbSideResolvedRenderModel.mjs'
18
+ } from './convergence/PcbSideResolvedRenderModel.mjs'
19
19
  export { PcbSvgRenderer } from './ui/PcbSvgRenderer.mjs'
20
20
  export { SchematicColorResolver } from './ui/SchematicColorResolver.mjs'
21
21
  export { SchematicContentLayout } from './ui/SchematicContentLayout.mjs'