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.
@@ -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
- return LegacySchematicSvgRenderer.render(
24
- SchematicSvgRenderer.#visibilityAwareDocument(normalized),
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
  /**