altium-toolkit 1.4.9 → 1.4.11
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.11.md +34 -0
- package/package.json +1 -1
- package/src/convergence/AltiumSchematicFidelityNormalizer.mjs +661 -0
- package/src/convergence/AltiumSchematicNativeFooterOwnerAligner.mjs +351 -0
- package/src/convergence/SchematicSvgRenderer.mjs +37 -2
- package/src/ui/SchematicHarnessRenderer.mjs +346 -0
- package/src/ui/SchematicRotatedOwnerTextPlacement.mjs +55 -0
|
@@ -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,9 @@
|
|
|
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'
|
|
7
|
+
import { AltiumSchematicFidelityNormalizer } from './AltiumSchematicFidelityNormalizer.mjs'
|
|
8
|
+
import { SchematicHarnessRenderer } from '../ui/SchematicHarnessRenderer.mjs'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Renders native Altium schematic models through the preserved historical
|
|
@@ -20,10 +23,42 @@ export class SchematicSvgRenderer {
|
|
|
20
23
|
static render(documentModel, options = {}) {
|
|
21
24
|
const normalized =
|
|
22
25
|
AltiumSchematicImageNormalizer.normalize(documentModel)
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const fidelityNormalized =
|
|
27
|
+
AltiumSchematicFidelityNormalizer.normalize(normalized)
|
|
28
|
+
const aligned =
|
|
29
|
+
AltiumSchematicNativeFooterOwnerAligner.align(fidelityNormalized)
|
|
30
|
+
const renderDocument =
|
|
31
|
+
SchematicSvgRenderer.#visibilityAwareDocument(aligned)
|
|
32
|
+
const markup = LegacySchematicSvgRenderer.render(
|
|
33
|
+
renderDocument,
|
|
25
34
|
options
|
|
26
35
|
)
|
|
36
|
+
|
|
37
|
+
return SchematicSvgRenderer.#injectHarnessMarkup(markup, renderDocument)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Inserts first-class harness markup into the preserved SVG hierarchy.
|
|
42
|
+
* @param {string} markup Historical schematic SVG markup.
|
|
43
|
+
* @param {Record<string, any>} documentModel Fidelity-normalized document.
|
|
44
|
+
* @returns {string} SVG markup with harness primitives.
|
|
45
|
+
*/
|
|
46
|
+
static #injectHarnessMarkup(markup, documentModel) {
|
|
47
|
+
if (markup.includes('class="schematic-harnesses"')) return markup
|
|
48
|
+
|
|
49
|
+
const schematic = documentModel?.schematic
|
|
50
|
+
const harnessMarkup = SchematicHarnessRenderer.buildMarkup(
|
|
51
|
+
schematic?.harnesses,
|
|
52
|
+
Number(schematic?.sheet?.height || 0),
|
|
53
|
+
schematic?.sheet || {}
|
|
54
|
+
)
|
|
55
|
+
if (!harnessMarkup) return markup
|
|
56
|
+
|
|
57
|
+
const marker = '<g class="schematic-images">'
|
|
58
|
+
return markup.replace(
|
|
59
|
+
marker,
|
|
60
|
+
'<g class="schematic-harnesses">' + harnessMarkup + '</g>' + marker
|
|
61
|
+
)
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
/**
|