altium-toolkit 1.4.8 → 1.4.10
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/docs/release-notes-v1.4.10.md +29 -0
- package/docs/release-notes-v1.4.9.md +37 -0
- package/package.json +1 -1
- package/src/convergence/AltiumSchematicNativeFooterOwnerAligner.mjs +351 -0
- package/src/convergence/PcbConvergenceLayerModel.mjs +213 -0
- package/src/convergence/PcbDrawingTextComposite.mjs +121 -0
- package/src/convergence/PcbSvgRenderer.mjs +18 -1
- package/src/convergence/PcbVisibleLayerViewport.mjs +129 -0
- package/src/convergence/SchematicSvgRenderer.mjs +4 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# altium-toolkit 1.4.10
|
|
2
|
+
|
|
3
|
+
Version 1.4.10 keeps complete authored schematic footer assemblies aligned when
|
|
4
|
+
a recovered standard sheet is wider than its stored native template.
|
|
5
|
+
|
|
6
|
+
## Native schematic footer alignment
|
|
7
|
+
|
|
8
|
+
- The convergence renderer detects structurally seeded native-footer owners
|
|
9
|
+
across every supported schematic primitive family.
|
|
10
|
+
- Lines, shapes, text, and embedded images belonging to the same authored owner
|
|
11
|
+
receive one shared effective right-edge translation.
|
|
12
|
+
- Multiple footer owners use the exact single offset computed by the preserved
|
|
13
|
+
historical renderer, while unrelated and ownerless content remains fixed.
|
|
14
|
+
|
|
15
|
+
## Compatibility
|
|
16
|
+
|
|
17
|
+
- Historical native renderer and partitioner sources remain byte-for-byte
|
|
18
|
+
frozen.
|
|
19
|
+
- The correction is a render-only convergence adaptation and does not mutate
|
|
20
|
+
parsed documents or change public parser and renderer signatures.
|
|
21
|
+
- Detection derives from promoted-sheet metadata, authored ownership, and
|
|
22
|
+
primitive geometry without matching project names, labels, or sample data.
|
|
23
|
+
|
|
24
|
+
## Verification
|
|
25
|
+
|
|
26
|
+
- Repository-owned fake regressions cover lower footer seeds, upper geometry,
|
|
27
|
+
text, embedded images, multiple seeded owners, and unrelated content.
|
|
28
|
+
- The complete package suite, formatting check, performance guard, npm package
|
|
29
|
+
dry run, and ECAD Forge integration gates are required for release.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# altium-toolkit 1.4.9
|
|
2
|
+
|
|
3
|
+
Version 1.4.9 restores complete mechanical and documentation artwork in native
|
|
4
|
+
Altium PCB rendering and supports fitting the viewport to visible layers.
|
|
5
|
+
|
|
6
|
+
## Drawing annotations
|
|
7
|
+
|
|
8
|
+
- Text from mechanical, assembly, fabrication, documentation, notes, dimension,
|
|
9
|
+
and courtyard layers is rendered alongside the corresponding drawing
|
|
10
|
+
geometry.
|
|
11
|
+
- Side-specific assembly annotations follow the active board side.
|
|
12
|
+
- Off-board drawing annotations remain outside the board clip, preserving title
|
|
13
|
+
blocks and fabrication notes at their authored positions.
|
|
14
|
+
|
|
15
|
+
## Visible-layer viewport
|
|
16
|
+
|
|
17
|
+
- PCB render options accept hidden layer aliases when calculating the root SVG
|
|
18
|
+
viewport.
|
|
19
|
+
- Hidden drawing layers retain their SVG markup for instant visibility toggles
|
|
20
|
+
while no longer expanding the fitted viewport.
|
|
21
|
+
- Component placements far outside the board are excluded from board-first
|
|
22
|
+
viewport fitting when drawing layers are hidden.
|
|
23
|
+
|
|
24
|
+
## Compatibility
|
|
25
|
+
|
|
26
|
+
- Historical native renderer source remains frozen; the behavior is composed in
|
|
27
|
+
the public convergence renderer.
|
|
28
|
+
- Existing render calls without hidden layers preserve their previous output and
|
|
29
|
+
bounds.
|
|
30
|
+
- Layer-only exports retain the historical output contract.
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
- Repository-owned fake regressions cover top- and bottom-side drawing text,
|
|
35
|
+
unclipped annotations, retained hidden markup, and visible-layer bounds.
|
|
36
|
+
- The complete package suite, formatting check, performance guard, npm package
|
|
37
|
+
dry run, and ECAD Forge integration gates are required for release.
|
package/package.json
CHANGED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
const PRIMITIVE_FAMILIES = Object.freeze([
|
|
5
|
+
'lines',
|
|
6
|
+
'polygons',
|
|
7
|
+
'rectangles',
|
|
8
|
+
'roundedRectangles',
|
|
9
|
+
'ellipses',
|
|
10
|
+
'arcs',
|
|
11
|
+
'beziers',
|
|
12
|
+
'pies',
|
|
13
|
+
'ieeeSymbols',
|
|
14
|
+
'texts',
|
|
15
|
+
'images'
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Aligns complete authored footer-owner groups before the preserved historical
|
|
20
|
+
* renderer partitions individual native footer primitives.
|
|
21
|
+
*/
|
|
22
|
+
export class AltiumSchematicNativeFooterOwnerAligner {
|
|
23
|
+
/**
|
|
24
|
+
* Builds a render-only schematic view whose non-seed footer-owner members
|
|
25
|
+
* receive the same effective translation as the historical footer group.
|
|
26
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
27
|
+
* @returns {Record<string, any>} Owner-aligned render document.
|
|
28
|
+
*/
|
|
29
|
+
static align(documentModel) {
|
|
30
|
+
const schematic = documentModel?.schematic
|
|
31
|
+
const sheet = schematic?.sheet
|
|
32
|
+
if (
|
|
33
|
+
!schematic ||
|
|
34
|
+
!AltiumSchematicNativeFooterOwnerAligner.#shouldAlign(sheet)
|
|
35
|
+
) {
|
|
36
|
+
return documentModel
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const seededOwners = new Set()
|
|
40
|
+
let seedMaximumX = -Infinity
|
|
41
|
+
for (const family of PRIMITIVE_FAMILIES) {
|
|
42
|
+
for (const primitive of schematic[family] || []) {
|
|
43
|
+
if (
|
|
44
|
+
!AltiumSchematicNativeFooterOwnerAligner.#isFooterSeed(
|
|
45
|
+
primitive,
|
|
46
|
+
sheet
|
|
47
|
+
)
|
|
48
|
+
) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const owner =
|
|
53
|
+
AltiumSchematicNativeFooterOwnerAligner.#owner(primitive)
|
|
54
|
+
const bounds =
|
|
55
|
+
AltiumSchematicNativeFooterOwnerAligner.#bounds(primitive)
|
|
56
|
+
seededOwners.add(owner)
|
|
57
|
+
seedMaximumX = Math.max(seedMaximumX, bounds.maxX)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!seededOwners.size) return documentModel
|
|
61
|
+
|
|
62
|
+
const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
|
|
63
|
+
const offset = Math.max(Number(sheet.width) - margin - seedMaximumX, 0)
|
|
64
|
+
if (!(offset > 0)) return documentModel
|
|
65
|
+
const alignedFamilies = {}
|
|
66
|
+
let changed = false
|
|
67
|
+
|
|
68
|
+
for (const family of PRIMITIVE_FAMILIES) {
|
|
69
|
+
const primitives = schematic[family]
|
|
70
|
+
if (!Array.isArray(primitives)) continue
|
|
71
|
+
|
|
72
|
+
let familyChanged = false
|
|
73
|
+
const alignedPrimitives = primitives.map((primitive) => {
|
|
74
|
+
if (
|
|
75
|
+
!seededOwners.has(
|
|
76
|
+
AltiumSchematicNativeFooterOwnerAligner.#owner(
|
|
77
|
+
primitive
|
|
78
|
+
)
|
|
79
|
+
) ||
|
|
80
|
+
AltiumSchematicNativeFooterOwnerAligner.#isFooterSeed(
|
|
81
|
+
primitive,
|
|
82
|
+
sheet
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
return primitive
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
familyChanged = true
|
|
89
|
+
return AltiumSchematicNativeFooterOwnerAligner.#translate(
|
|
90
|
+
primitive,
|
|
91
|
+
offset
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (familyChanged) {
|
|
96
|
+
alignedFamilies[family] = alignedPrimitives
|
|
97
|
+
changed = true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!changed) return documentModel
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...documentModel,
|
|
105
|
+
schematic: {
|
|
106
|
+
...schematic,
|
|
107
|
+
...alignedFamilies
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns true when a promoted standard sheet requires native-footer
|
|
114
|
+
* alignment.
|
|
115
|
+
* @param {Record<string, any> | undefined} sheet Sheet metadata.
|
|
116
|
+
* @returns {boolean} Whether alignment can apply.
|
|
117
|
+
*/
|
|
118
|
+
static #shouldAlign(sheet) {
|
|
119
|
+
const width = Number(sheet?.width || 0)
|
|
120
|
+
const height = Number(sheet?.height || 0)
|
|
121
|
+
const sourceWidth = Number(sheet?.sourceWidth || 0)
|
|
122
|
+
const sourceHeight = Number(sheet?.sourceHeight || 0)
|
|
123
|
+
|
|
124
|
+
return Boolean(
|
|
125
|
+
sheet?.borderOn &&
|
|
126
|
+
sheet?.paperSize &&
|
|
127
|
+
sourceWidth > 0 &&
|
|
128
|
+
sourceHeight > 0 &&
|
|
129
|
+
(width > sourceWidth || height > sourceHeight)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns true when one primitive triggers the preserved lower-right
|
|
135
|
+
* native-footer partitioning predicate.
|
|
136
|
+
* @param {Record<string, any>} primitive Primitive candidate.
|
|
137
|
+
* @param {Record<string, any>} sheet Sheet metadata.
|
|
138
|
+
* @returns {boolean} Whether the primitive seeds a footer owner.
|
|
139
|
+
*/
|
|
140
|
+
static #isFooterSeed(primitive, sheet) {
|
|
141
|
+
if (!AltiumSchematicNativeFooterOwnerAligner.#owner(primitive)) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const bounds =
|
|
146
|
+
AltiumSchematicNativeFooterOwnerAligner.#bounds(primitive)
|
|
147
|
+
if (!bounds) return false
|
|
148
|
+
|
|
149
|
+
const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
|
|
150
|
+
const footerLimit = Math.max(margin * 6, 120)
|
|
151
|
+
const footerStartX = Math.max(Number(sheet?.width || 0), 0) * 0.5
|
|
152
|
+
|
|
153
|
+
return bounds.maxY <= footerLimit && bounds.maxX >= footerStartX
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Normalizes one primitive owner for structural grouping.
|
|
158
|
+
* @param {Record<string, any> | undefined} primitive Primitive candidate.
|
|
159
|
+
* @returns {string} Normalized owner key.
|
|
160
|
+
*/
|
|
161
|
+
static #owner(primitive) {
|
|
162
|
+
return String(primitive?.ownerIndex || '').trim()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolves the document-space bounds of one normalized primitive.
|
|
167
|
+
* @param {Record<string, any>} primitive Primitive candidate.
|
|
168
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null} Primitive bounds.
|
|
169
|
+
*/
|
|
170
|
+
static #bounds(primitive) {
|
|
171
|
+
const points =
|
|
172
|
+
AltiumSchematicNativeFooterOwnerAligner.#primitivePoints(primitive)
|
|
173
|
+
if (!points.length) return null
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
minX: Math.min(...points.map((point) => point.x)),
|
|
177
|
+
minY: Math.min(...points.map((point) => point.y)),
|
|
178
|
+
maxX: Math.max(...points.map((point) => point.x)),
|
|
179
|
+
maxY: Math.max(...points.map((point) => point.y))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Collects finite point coordinates from one normalized primitive.
|
|
185
|
+
* @param {Record<string, any>} primitive Primitive candidate.
|
|
186
|
+
* @returns {{ x: number, y: number }[]} Primitive points.
|
|
187
|
+
*/
|
|
188
|
+
static #primitivePoints(primitive) {
|
|
189
|
+
if (Array.isArray(primitive?.points)) {
|
|
190
|
+
return primitive.points
|
|
191
|
+
.map((point) =>
|
|
192
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
193
|
+
point?.x,
|
|
194
|
+
point?.y
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(primitive?.segments)) {
|
|
200
|
+
return primitive.segments.flatMap((segment) =>
|
|
201
|
+
[
|
|
202
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
203
|
+
segment?.x1,
|
|
204
|
+
segment?.y1
|
|
205
|
+
),
|
|
206
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
207
|
+
segment?.x2,
|
|
208
|
+
segment?.y2
|
|
209
|
+
)
|
|
210
|
+
].filter(Boolean)
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
Number.isFinite(Number(primitive?.x1)) &&
|
|
215
|
+
Number.isFinite(Number(primitive?.y1)) &&
|
|
216
|
+
Number.isFinite(Number(primitive?.x2)) &&
|
|
217
|
+
Number.isFinite(Number(primitive?.y2))
|
|
218
|
+
) {
|
|
219
|
+
return [
|
|
220
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
221
|
+
primitive.x1,
|
|
222
|
+
primitive.y1
|
|
223
|
+
),
|
|
224
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
225
|
+
primitive.x2,
|
|
226
|
+
primitive.y2
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
if (
|
|
231
|
+
Number.isFinite(Number(primitive?.cornerX)) &&
|
|
232
|
+
Number.isFinite(Number(primitive?.cornerY))
|
|
233
|
+
) {
|
|
234
|
+
return [
|
|
235
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
236
|
+
primitive.x,
|
|
237
|
+
primitive.y
|
|
238
|
+
),
|
|
239
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
240
|
+
primitive.cornerX,
|
|
241
|
+
primitive.cornerY
|
|
242
|
+
)
|
|
243
|
+
].filter(Boolean)
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
Number.isFinite(Number(primitive?.width)) &&
|
|
247
|
+
Number.isFinite(Number(primitive?.height))
|
|
248
|
+
) {
|
|
249
|
+
return [
|
|
250
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
251
|
+
primitive.x,
|
|
252
|
+
primitive.y
|
|
253
|
+
),
|
|
254
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
255
|
+
Number(primitive.x || 0) + Number(primitive.width || 0),
|
|
256
|
+
Number(primitive.y || 0) + Number(primitive.height || 0)
|
|
257
|
+
)
|
|
258
|
+
].filter(Boolean)
|
|
259
|
+
}
|
|
260
|
+
if (
|
|
261
|
+
Number.isFinite(Number(primitive?.radius)) ||
|
|
262
|
+
Number.isFinite(Number(primitive?.radiusX))
|
|
263
|
+
) {
|
|
264
|
+
const radiusX = Math.max(
|
|
265
|
+
Number(primitive?.radiusX || primitive?.radius || 0),
|
|
266
|
+
0
|
|
267
|
+
)
|
|
268
|
+
const radiusY = Math.max(
|
|
269
|
+
Number(primitive?.radiusY || primitive?.radius || 0),
|
|
270
|
+
0
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return [
|
|
274
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
275
|
+
Number(primitive.x || 0) - radiusX,
|
|
276
|
+
Number(primitive.y || 0) - radiusY
|
|
277
|
+
),
|
|
278
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
279
|
+
Number(primitive.x || 0) + radiusX,
|
|
280
|
+
Number(primitive.y || 0) + radiusY
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
AltiumSchematicNativeFooterOwnerAligner.#point(
|
|
287
|
+
primitive?.x,
|
|
288
|
+
primitive?.y
|
|
289
|
+
)
|
|
290
|
+
].filter(Boolean)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Builds one finite point.
|
|
295
|
+
* @param {number | string | undefined} x Raw x coordinate.
|
|
296
|
+
* @param {number | string | undefined} y Raw y coordinate.
|
|
297
|
+
* @returns {{ x: number, y: number } | null} Finite point.
|
|
298
|
+
*/
|
|
299
|
+
static #point(x, y) {
|
|
300
|
+
const normalizedX = Number(x)
|
|
301
|
+
const normalizedY = Number(y)
|
|
302
|
+
if (!Number.isFinite(normalizedX) || !Number.isFinite(normalizedY)) {
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { x: normalizedX, y: normalizedY }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Copies one primitive while translating each present horizontal geometry
|
|
311
|
+
* coordinate.
|
|
312
|
+
* @param {Record<string, any>} primitive Primitive to translate.
|
|
313
|
+
* @param {number} offset Positive horizontal offset.
|
|
314
|
+
* @returns {Record<string, any>} Translated primitive.
|
|
315
|
+
*/
|
|
316
|
+
static #translate(primitive, offset) {
|
|
317
|
+
const translated = { ...primitive }
|
|
318
|
+
for (const field of ['x', 'x1', 'x2', 'cornerX']) {
|
|
319
|
+
if (
|
|
320
|
+
Object.hasOwn(primitive, field) &&
|
|
321
|
+
Number.isFinite(Number(primitive[field]))
|
|
322
|
+
) {
|
|
323
|
+
translated[field] = Number(primitive[field]) + offset
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (Array.isArray(primitive.points)) {
|
|
327
|
+
translated.points = primitive.points.map((point) => ({
|
|
328
|
+
...point,
|
|
329
|
+
...(Number.isFinite(Number(point?.x))
|
|
330
|
+
? { x: Number(point.x) + offset }
|
|
331
|
+
: {})
|
|
332
|
+
}))
|
|
333
|
+
}
|
|
334
|
+
if (Array.isArray(primitive.segments)) {
|
|
335
|
+
translated.segments = primitive.segments.map((segment) => ({
|
|
336
|
+
...segment,
|
|
337
|
+
...(Number.isFinite(Number(segment?.x1))
|
|
338
|
+
? { x1: Number(segment.x1) + offset }
|
|
339
|
+
: {}),
|
|
340
|
+
...(Number.isFinite(Number(segment?.x2))
|
|
341
|
+
? { x2: Number(segment.x2) + offset }
|
|
342
|
+
: {})
|
|
343
|
+
}))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return translated
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
Object.freeze(AltiumSchematicNativeFooterOwnerAligner.prototype)
|
|
351
|
+
Object.freeze(AltiumSchematicNativeFooterOwnerAligner)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { PcbLayerGroups } from '../core/altium/PcbLayerGroups.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves normalized layer identity for convergence-owned PCB rendering.
|
|
8
|
+
*/
|
|
9
|
+
export class PcbConvergenceLayerModel {
|
|
10
|
+
/**
|
|
11
|
+
* Resolves distinct layer descriptors from stack and primitive metadata.
|
|
12
|
+
* @param {object} documentModel PCB document.
|
|
13
|
+
* @returns {object[]}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(documentModel) {
|
|
16
|
+
const pcb = documentModel?.pcb || {}
|
|
17
|
+
const layers = [...(pcb.layers || []), ...(pcb.primitiveLayers || [])]
|
|
18
|
+
const descriptors = []
|
|
19
|
+
const identities = new Set()
|
|
20
|
+
|
|
21
|
+
for (const layer of layers) {
|
|
22
|
+
const descriptor = PcbConvergenceLayerModel.#descriptor(layer)
|
|
23
|
+
const identity = descriptor.layerKey || descriptor.displayName
|
|
24
|
+
if (!identity || identities.has(identity)) continue
|
|
25
|
+
identities.add(identity)
|
|
26
|
+
descriptors.push(descriptor)
|
|
27
|
+
}
|
|
28
|
+
return descriptors
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds layer lookup maps for primitive and text matching.
|
|
33
|
+
* @param {object} documentModel PCB document.
|
|
34
|
+
* @returns {{ byId: Map<number, object>, byName: Map<string, object>, layers: object[] }}
|
|
35
|
+
*/
|
|
36
|
+
static buildLookup(documentModel) {
|
|
37
|
+
const layers = PcbConvergenceLayerModel.resolve(documentModel)
|
|
38
|
+
const byId = new Map()
|
|
39
|
+
const byName = new Map()
|
|
40
|
+
for (const layer of layers) {
|
|
41
|
+
for (const id of [layer.layerId, layer.legacyLayerId]) {
|
|
42
|
+
if (Number.isInteger(id) && !byId.has(id)) byId.set(id, layer)
|
|
43
|
+
}
|
|
44
|
+
byName.set(
|
|
45
|
+
PcbConvergenceLayerModel.normalize(layer.displayName),
|
|
46
|
+
layer
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return { byId, byName, layers }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the known layer for one primitive.
|
|
54
|
+
* @param {object} primitive Primitive record.
|
|
55
|
+
* @param {{ byId: Map<number, object>, byName: Map<string, object> }} lookup Layer lookup.
|
|
56
|
+
* @returns {object | null}
|
|
57
|
+
*/
|
|
58
|
+
static layerForPrimitive(primitive, lookup) {
|
|
59
|
+
for (const value of [primitive?.layerId, primitive?.layerCode]) {
|
|
60
|
+
const id = Number(value)
|
|
61
|
+
if (Number.isInteger(id) && lookup.byId.has(id)) {
|
|
62
|
+
return lookup.byId.get(id)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const name = PcbConvergenceLayerModel.normalize(
|
|
66
|
+
primitive?.layerName || primitive?.layer || primitive?.side
|
|
67
|
+
)
|
|
68
|
+
return name ? lookup.byName.get(name) || null : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns all stable aliases for one layer descriptor.
|
|
73
|
+
* @param {object | null} layer Layer descriptor.
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
static aliases(layer) {
|
|
77
|
+
if (!layer) return []
|
|
78
|
+
return [
|
|
79
|
+
layer.layerKey,
|
|
80
|
+
layer.displayName,
|
|
81
|
+
layer.layerId,
|
|
82
|
+
layer.legacyLayerId,
|
|
83
|
+
Number.isInteger(layer.layerId) ? 'L' + layer.layerId : '',
|
|
84
|
+
Number.isInteger(layer.legacyLayerId)
|
|
85
|
+
? 'L' + layer.legacyLayerId
|
|
86
|
+
: ''
|
|
87
|
+
]
|
|
88
|
+
.map(PcbConvergenceLayerModel.normalize)
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns true for mechanical and documentation drawing layers.
|
|
94
|
+
* @param {object} layer Layer descriptor.
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
static isDrawingLayer(layer) {
|
|
98
|
+
const text = [layer?.displayName, layer?.role]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join(' ')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
return /(mechanical|assembly|\basm\b|fabrication|\bfab\b|drawing|dimension|documentation|document|notes?|courtyard|crtyd)/u.test(
|
|
103
|
+
text
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns true when one drawing layer belongs to the requested side.
|
|
109
|
+
* @param {object} layer Layer descriptor.
|
|
110
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
static isDrawingLayerForSide(layer, side) {
|
|
114
|
+
if (!PcbConvergenceLayerModel.isDrawingLayer(layer)) return false
|
|
115
|
+
const text = [layer?.displayName, layer?.role]
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join(' ')
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
const compact = text.replace(/[^a-z0-9]+/gu, '')
|
|
120
|
+
const bottom =
|
|
121
|
+
/\bbottom\b|\bbot\b|botside|backassembly|bassembly|bcrtyd/u.test(
|
|
122
|
+
text
|
|
123
|
+
) || compact.includes('assemblybottom')
|
|
124
|
+
const top =
|
|
125
|
+
/\btop\b|frontassembly|fassembly|fcrtyd/u.test(text) ||
|
|
126
|
+
compact.includes('assemblytop')
|
|
127
|
+
|
|
128
|
+
if (bottom && !top) return side === 'bottom'
|
|
129
|
+
if (top && !bottom) return side === 'top'
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalizes one semantic layer alias.
|
|
135
|
+
* @param {unknown} value Raw alias.
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
static normalize(value) {
|
|
139
|
+
return String(value ?? '')
|
|
140
|
+
.trim()
|
|
141
|
+
.toUpperCase()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds one normalized descriptor.
|
|
146
|
+
* @param {object} layer Source layer.
|
|
147
|
+
* @returns {object}
|
|
148
|
+
*/
|
|
149
|
+
static #descriptor(layer) {
|
|
150
|
+
const layerId = PcbConvergenceLayerModel.#firstInteger([
|
|
151
|
+
layer?.layerId,
|
|
152
|
+
layer?.id,
|
|
153
|
+
layer?.index,
|
|
154
|
+
layer?.number
|
|
155
|
+
])
|
|
156
|
+
const legacyLayerId = PcbConvergenceLayerModel.#firstInteger([
|
|
157
|
+
layer?.legacyLayerId,
|
|
158
|
+
layer?.legacyId
|
|
159
|
+
])
|
|
160
|
+
const displayName = String(
|
|
161
|
+
layer?.displayName || layer?.name || layer?.label || ''
|
|
162
|
+
)
|
|
163
|
+
return {
|
|
164
|
+
layerId,
|
|
165
|
+
legacyLayerId,
|
|
166
|
+
layerKey: Number.isInteger(layerId)
|
|
167
|
+
? 'L' + layerId
|
|
168
|
+
: PcbConvergenceLayerModel.normalize(displayName),
|
|
169
|
+
displayName,
|
|
170
|
+
role:
|
|
171
|
+
layer?.role ||
|
|
172
|
+
layer?.layerRole ||
|
|
173
|
+
PcbConvergenceLayerModel.#inferRole(
|
|
174
|
+
displayName,
|
|
175
|
+
legacyLayerId ?? layerId
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolves a broad layer role.
|
|
182
|
+
* @param {string} name Layer name.
|
|
183
|
+
* @param {number | undefined} layerId Layer id.
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
static #inferRole(name, layerId) {
|
|
187
|
+
if (PcbLayerGroups.isMechanical(layerId)) return 'mechanical'
|
|
188
|
+
if (PcbLayerGroups.isOverlay(layerId)) return 'overlay'
|
|
189
|
+
if (PcbLayerGroups.isCopper(layerId)) return 'copper'
|
|
190
|
+
const normalized = name.toLowerCase()
|
|
191
|
+
if (/mechanical|dimension|drawing/u.test(normalized)) {
|
|
192
|
+
return 'mechanical'
|
|
193
|
+
}
|
|
194
|
+
if (/assembly|\basm\b/u.test(normalized)) return 'assembly'
|
|
195
|
+
if (/notes?|document|courtyard|crtyd/u.test(normalized)) {
|
|
196
|
+
return 'documentation'
|
|
197
|
+
}
|
|
198
|
+
return 'other'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the first integer in a value list.
|
|
203
|
+
* @param {unknown[]} values Candidate values.
|
|
204
|
+
* @returns {number | undefined}
|
|
205
|
+
*/
|
|
206
|
+
static #firstInteger(values) {
|
|
207
|
+
for (const value of values) {
|
|
208
|
+
const number = Number(value)
|
|
209
|
+
if (Number.isInteger(number)) return number
|
|
210
|
+
}
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { PcbTextPrimitiveRenderer } from '../ui/PcbTextPrimitiveRenderer.mjs'
|
|
5
|
+
import { PcbConvergenceLayerModel } from './PcbConvergenceLayerModel.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adds side-correct drawing annotations outside the board-only text clip.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbDrawingTextComposite {
|
|
11
|
+
/**
|
|
12
|
+
* Adds mechanical and documentation text to rendered PCB markup.
|
|
13
|
+
* @param {string} markup Historical renderer markup.
|
|
14
|
+
* @param {object} documentModel PCB document.
|
|
15
|
+
* @param {{ side?: 'top' | 'bottom' }} [options] Render options.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
static apply(markup, documentModel, options = {}) {
|
|
19
|
+
const pcb = documentModel?.pcb
|
|
20
|
+
if (!pcb) return markup
|
|
21
|
+
const side = options?.side === 'bottom' ? 'bottom' : 'top'
|
|
22
|
+
const lookup = PcbConvergenceLayerModel.buildLookup(documentModel)
|
|
23
|
+
const drawingLayers = lookup.layers.filter((layer) =>
|
|
24
|
+
PcbConvergenceLayerModel.isDrawingLayerForSide(layer, side)
|
|
25
|
+
)
|
|
26
|
+
if (!drawingLayers.length) return markup
|
|
27
|
+
|
|
28
|
+
const selectorLayers = drawingLayers
|
|
29
|
+
.filter((layer) => Number.isInteger(layer.layerId))
|
|
30
|
+
.map((layer) => ({
|
|
31
|
+
layerId: layer.layerId,
|
|
32
|
+
name: 'Top Overlay'
|
|
33
|
+
}))
|
|
34
|
+
const texts = PcbTextPrimitiveRenderer.select(
|
|
35
|
+
selectorLayers,
|
|
36
|
+
pcb.texts || [],
|
|
37
|
+
'top'
|
|
38
|
+
)
|
|
39
|
+
if (!texts.length) return markup
|
|
40
|
+
|
|
41
|
+
const textMarkup = PcbTextPrimitiveRenderer.render(texts, {
|
|
42
|
+
semanticContext: PcbDrawingTextComposite.#semanticContext(
|
|
43
|
+
pcb,
|
|
44
|
+
lookup
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
return PcbDrawingTextComposite.#insertDrawingGroup(markup, textMarkup)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds the public semantic context consumed by the historical text
|
|
52
|
+
* primitive renderer.
|
|
53
|
+
* @param {object} pcb PCB model.
|
|
54
|
+
* @param {{ byId: Map<number, object> }} lookup Layer lookup.
|
|
55
|
+
* @returns {object}
|
|
56
|
+
*/
|
|
57
|
+
static #semanticContext(pcb, lookup) {
|
|
58
|
+
return {
|
|
59
|
+
layersById: lookup.byId,
|
|
60
|
+
primitiveIndexes: {
|
|
61
|
+
texts: new Map(
|
|
62
|
+
(pcb.texts || []).map((text, index) => [text, index])
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
netByIndex: new Map(
|
|
66
|
+
(pcb.nets || []).map((net) => [Number(net.netIndex), net])
|
|
67
|
+
),
|
|
68
|
+
netClassNamesByNetName: new Map(),
|
|
69
|
+
componentsByIndex: new Map(
|
|
70
|
+
(pcb.components || []).map((component) => [
|
|
71
|
+
Number(component.componentIndex),
|
|
72
|
+
component
|
|
73
|
+
])
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Inserts one un-clipped group immediately after the ordinary text group.
|
|
80
|
+
* @param {string} markup SVG panel markup.
|
|
81
|
+
* @param {string} textMarkup Drawing text markup.
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
static #insertDrawingGroup(markup, textMarkup) {
|
|
85
|
+
const start = markup.indexOf('<g class="pcb-texts"')
|
|
86
|
+
if (start < 0) return markup
|
|
87
|
+
const openEnd = markup.indexOf('>', start)
|
|
88
|
+
const end = PcbDrawingTextComposite.#groupEnd(markup, openEnd + 1)
|
|
89
|
+
if (openEnd < 0 || end < 0) return markup
|
|
90
|
+
const openTag = markup.slice(start, openEnd + 1)
|
|
91
|
+
const transform = openTag.match(/\stransform="[^"]*"/u)?.[0] || ''
|
|
92
|
+
const group =
|
|
93
|
+
'<g class="pcb-drawing-texts"' +
|
|
94
|
+
transform +
|
|
95
|
+
'>' +
|
|
96
|
+
textMarkup +
|
|
97
|
+
'</g>'
|
|
98
|
+
return markup.slice(0, end) + group + markup.slice(end)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Finds the offset after the matching close tag for one SVG group.
|
|
103
|
+
* @param {string} markup SVG markup.
|
|
104
|
+
* @param {number} contentStart Group content offset.
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
static #groupEnd(markup, contentStart) {
|
|
108
|
+
const pattern = /<g\b[^>]*>|<\/g>/gu
|
|
109
|
+
let depth = 1
|
|
110
|
+
pattern.lastIndex = contentStart
|
|
111
|
+
for (
|
|
112
|
+
let match = pattern.exec(markup);
|
|
113
|
+
match;
|
|
114
|
+
match = pattern.exec(markup)
|
|
115
|
+
) {
|
|
116
|
+
depth += match[0].startsWith('</') ? -1 : 1
|
|
117
|
+
if (depth === 0) return pattern.lastIndex
|
|
118
|
+
}
|
|
119
|
+
return -1
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
import { PcbSvgRenderer as LegacyPcbSvgRenderer } from '../ui/PcbSvgRenderer.mjs'
|
|
5
|
+
import { PcbDrawingTextComposite } from './PcbDrawingTextComposite.mjs'
|
|
6
|
+
import { PcbVisibleLayerViewport } from './PcbVisibleLayerViewport.mjs'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Renders native Altium PCB models through the preserved historical renderer
|
|
@@ -19,7 +21,22 @@ export class PcbSvgRenderer {
|
|
|
19
21
|
* @returns {string} Rendered SVG panel markup.
|
|
20
22
|
*/
|
|
21
23
|
static render(documentModel, options = {}) {
|
|
22
|
-
const
|
|
24
|
+
const historicalMarkup = LegacyPcbSvgRenderer.render(
|
|
25
|
+
documentModel,
|
|
26
|
+
options
|
|
27
|
+
)
|
|
28
|
+
const drawingMarkup = PcbDrawingTextComposite.apply(
|
|
29
|
+
historicalMarkup,
|
|
30
|
+
documentModel,
|
|
31
|
+
options
|
|
32
|
+
)
|
|
33
|
+
const markup = PcbVisibleLayerViewport.apply(
|
|
34
|
+
drawingMarkup,
|
|
35
|
+
documentModel,
|
|
36
|
+
options,
|
|
37
|
+
(filteredDocument, filteredOptions) =>
|
|
38
|
+
LegacyPcbSvgRenderer.render(filteredDocument, filteredOptions)
|
|
39
|
+
)
|
|
23
40
|
const subsurfacePadIndexes = PcbSvgRenderer.#subsurfacePadIndexes(
|
|
24
41
|
documentModel,
|
|
25
42
|
options
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { PcbConvergenceLayerModel } from './PcbConvergenceLayerModel.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Replaces PCB viewport bounds with bounds rendered from visible layers.
|
|
8
|
+
*/
|
|
9
|
+
export class PcbVisibleLayerViewport {
|
|
10
|
+
/**
|
|
11
|
+
* Applies visible-layer viewBox bounds while retaining original SVG markup.
|
|
12
|
+
* @param {string} markup Complete SVG panel markup.
|
|
13
|
+
* @param {object} documentModel PCB document.
|
|
14
|
+
* @param {{ hiddenLayers?: (string | number)[] }} options Render options.
|
|
15
|
+
* @param {(documentModel: object, options: object) => string} renderHistorical Historical render callback.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
static apply(markup, documentModel, options, renderHistorical) {
|
|
19
|
+
const hidden = new Set(
|
|
20
|
+
(Array.isArray(options?.hiddenLayers) ? options.hiddenLayers : [])
|
|
21
|
+
.map(PcbConvergenceLayerModel.normalize)
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
)
|
|
24
|
+
if (!hidden.size || !documentModel?.pcb) return markup
|
|
25
|
+
|
|
26
|
+
const filteredDocument = PcbVisibleLayerViewport.#filterDocument(
|
|
27
|
+
documentModel,
|
|
28
|
+
hidden
|
|
29
|
+
)
|
|
30
|
+
const filteredMarkup = renderHistorical(filteredDocument, {
|
|
31
|
+
...options,
|
|
32
|
+
hiddenLayers: undefined
|
|
33
|
+
})
|
|
34
|
+
const viewBox = filteredMarkup.match(
|
|
35
|
+
/<svg class="pcb-svg" viewBox="([^"]+)"/u
|
|
36
|
+
)?.[1]
|
|
37
|
+
if (!viewBox) return markup
|
|
38
|
+
|
|
39
|
+
return markup.replace(
|
|
40
|
+
/(<svg class="pcb-svg"[^>]*\sviewBox=")[^"]+("[^>]*>)/u,
|
|
41
|
+
'$1' + viewBox + '$2'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a shallow document clone with hidden-layer primitives removed.
|
|
47
|
+
* @param {object} documentModel Source document.
|
|
48
|
+
* @param {Set<string>} hidden Hidden layer aliases.
|
|
49
|
+
* @returns {object}
|
|
50
|
+
*/
|
|
51
|
+
static #filterDocument(documentModel, hidden) {
|
|
52
|
+
const pcb = documentModel.pcb
|
|
53
|
+
const lookup = PcbConvergenceLayerModel.buildLookup(documentModel)
|
|
54
|
+
const visible = (primitive) =>
|
|
55
|
+
!PcbVisibleLayerViewport.#isHidden(primitive, lookup, hidden)
|
|
56
|
+
const nearBoard = (component) =>
|
|
57
|
+
PcbVisibleLayerViewport.#isComponentNearBoard(
|
|
58
|
+
component,
|
|
59
|
+
pcb.boardOutline
|
|
60
|
+
)
|
|
61
|
+
const filterKeys = [
|
|
62
|
+
'polygons',
|
|
63
|
+
'fills',
|
|
64
|
+
'tracks',
|
|
65
|
+
'arcs',
|
|
66
|
+
'regions',
|
|
67
|
+
'shapeBasedRegions',
|
|
68
|
+
'vias',
|
|
69
|
+
'pads',
|
|
70
|
+
'texts',
|
|
71
|
+
'dimensions'
|
|
72
|
+
]
|
|
73
|
+
const filteredPcb = { ...pcb }
|
|
74
|
+
for (const key of filterKeys) {
|
|
75
|
+
filteredPcb[key] = (pcb[key] || []).filter(visible)
|
|
76
|
+
}
|
|
77
|
+
filteredPcb.components = (pcb.components || []).filter(
|
|
78
|
+
(component) => visible(component) && nearBoard(component)
|
|
79
|
+
)
|
|
80
|
+
return { ...documentModel, pcb: filteredPcb }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns true when one primitive belongs to a hidden layer.
|
|
85
|
+
* @param {object} primitive Primitive record.
|
|
86
|
+
* @param {object} lookup Layer lookup.
|
|
87
|
+
* @param {Set<string>} hidden Hidden aliases.
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
static #isHidden(primitive, lookup, hidden) {
|
|
91
|
+
const layer = PcbConvergenceLayerModel.layerForPrimitive(
|
|
92
|
+
primitive,
|
|
93
|
+
lookup
|
|
94
|
+
)
|
|
95
|
+
const aliases = [
|
|
96
|
+
...PcbConvergenceLayerModel.aliases(layer),
|
|
97
|
+
primitive?.layer,
|
|
98
|
+
primitive?.layerName,
|
|
99
|
+
primitive?.layerId,
|
|
100
|
+
primitive?.layerCode
|
|
101
|
+
]
|
|
102
|
+
.map(PcbConvergenceLayerModel.normalize)
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
return aliases.some((alias) => hidden.has(alias))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns true for placements near enough to affect a board-first fit.
|
|
109
|
+
* @param {object} component Component placement.
|
|
110
|
+
* @param {object} outline Board outline.
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
static #isComponentNearBoard(component, outline) {
|
|
114
|
+
const x = Number(component?.x)
|
|
115
|
+
const y = Number(component?.y)
|
|
116
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return true
|
|
117
|
+
const minX = Number(outline?.minX || 0)
|
|
118
|
+
const minY = Number(outline?.minY || 0)
|
|
119
|
+
const maxX = minX + Number(outline?.widthMil || 0)
|
|
120
|
+
const maxY = minY + Number(outline?.heightMil || 0)
|
|
121
|
+
const tolerance = 240
|
|
122
|
+
return (
|
|
123
|
+
x >= minX - tolerance &&
|
|
124
|
+
x <= maxX + tolerance &&
|
|
125
|
+
y >= minY - tolerance &&
|
|
126
|
+
y <= maxY + tolerance
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { SchematicSvgRenderer as LegacySchematicSvgRenderer } from '../ui/SchematicSvgRenderer.mjs'
|
|
5
5
|
import { AltiumSchematicImageNormalizer } from './AltiumSchematicImageNormalizer.mjs'
|
|
6
|
+
import { AltiumSchematicNativeFooterOwnerAligner } from './AltiumSchematicNativeFooterOwnerAligner.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Renders native Altium schematic models through the preserved historical
|
|
@@ -20,8 +21,10 @@ export class SchematicSvgRenderer {
|
|
|
20
21
|
static render(documentModel, options = {}) {
|
|
21
22
|
const normalized =
|
|
22
23
|
AltiumSchematicImageNormalizer.normalize(documentModel)
|
|
24
|
+
const aligned =
|
|
25
|
+
AltiumSchematicNativeFooterOwnerAligner.align(normalized)
|
|
23
26
|
return LegacySchematicSvgRenderer.render(
|
|
24
|
-
SchematicSvgRenderer.#visibilityAwareDocument(
|
|
27
|
+
SchematicSvgRenderer.#visibilityAwareDocument(aligned),
|
|
25
28
|
options
|
|
26
29
|
)
|
|
27
30
|
}
|