altium-toolkit 1.4.9 → 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.
|
@@ -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.
|
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)
|
|
@@ -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
|
}
|