altium-toolkit 1.1.2 → 1.1.22

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.
Files changed (57) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  8. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  9. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  10. package/src/core/altium/AltiumParser.mjs +245 -10
  11. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  12. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  13. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  14. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  15. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  16. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  17. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  18. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  19. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  20. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  21. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  22. package/src/core/altium/SchematicImageParser.mjs +291 -6
  23. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  24. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  25. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  26. package/src/core/altium/SchematicPinParser.mjs +262 -24
  27. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  28. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  29. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  30. package/src/core/altium/SchematicTextParser.mjs +125 -11
  31. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  32. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  33. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  34. package/src/core/altium/SourceComponentClient.mjs +239 -0
  35. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  36. package/src/parser.mjs +8 -0
  37. package/src/styles/altium-renderers.css +6 -6
  38. package/src/ui/PcbArcUtils.mjs +19 -2
  39. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  40. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  41. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  42. package/src/ui/SchematicColorResolver.mjs +263 -0
  43. package/src/ui/SchematicContentLayout.mjs +58 -1
  44. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  45. package/src/ui/SchematicImageRenderer.mjs +125 -10
  46. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  47. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  48. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  49. package/src/ui/SchematicNoteRenderer.mjs +87 -7
  50. package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
  51. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  52. package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
  53. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  54. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  55. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  56. package/src/ui/SchematicShapeRenderer.mjs +109 -24
  57. package/src/ui/SchematicSvgRenderer.mjs +1210 -71
@@ -4,9 +4,11 @@
4
4
 
5
5
  import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
6
  import { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+ import { SchematicPowerDiagramImageProcessor } from './SchematicPowerDiagramImageProcessor.mjs'
7
8
 
8
9
  const { escapeHtml, formatNumber, projectSchematicY } = SchematicSvgUtils
9
10
  const MISSING_IMAGE_WRAP_SAFETY_FACTOR = 0.96
11
+ const BLUEPRINT_IMAGE_FILTER_ID = 'schematic-blueprint-image-filter'
10
12
 
11
13
  /**
12
14
  * Renders normalized schematic image placements.
@@ -16,15 +18,19 @@ export class SchematicImageRenderer {
16
18
  * Builds markup for embedded schematic images and unresolved placeholders.
17
19
  * @param {{ x: number, y: number, cornerX: number, cornerY: number, fileName?: string, mimeType?: string, dataBase64?: string, diagnosticState?: string, keepAspect?: boolean }[]} images
18
20
  * @param {number} sheetHeight
21
+ * @param {{ colorizeImages?: boolean, colorize_images?: boolean }} options Image render options.
19
22
  * @returns {string}
20
23
  */
21
- static buildMarkup(images, sheetHeight) {
22
- return images
24
+ static buildMarkup(images, sheetHeight, options = {}) {
25
+ const renderOptions =
26
+ SchematicImageRenderer.#normalizeRenderOptions(options)
27
+ const imageMarkup = images
23
28
  .map((image) =>
24
29
  image.dataBase64 && image.mimeType
25
30
  ? SchematicImageRenderer.#buildEmbeddedImageMarkup(
26
31
  image,
27
- sheetHeight
32
+ sheetHeight,
33
+ renderOptions
28
34
  )
29
35
  : SchematicImageRenderer.#buildPlaceholderMarkup(
30
36
  image,
@@ -32,19 +38,82 @@ export class SchematicImageRenderer {
32
38
  )
33
39
  )
34
40
  .join('')
41
+
42
+ return (
43
+ SchematicImageRenderer.#buildImageFilterMarkup(
44
+ images,
45
+ renderOptions
46
+ ) + imageMarkup
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Normalizes image renderer options.
52
+ * @param {{ colorizeImages?: boolean, colorize_images?: boolean }} options Raw options.
53
+ * @returns {{ colorizeImages: boolean }}
54
+ */
55
+ static #normalizeRenderOptions(options) {
56
+ return {
57
+ colorizeImages:
58
+ options?.colorizeImages === true ||
59
+ options?.colorize_images === true
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Builds any SVG filters needed by the embedded image set.
65
+ * @param {{ x: number, y: number, cornerX: number, cornerY: number, fileName?: string, mimeType?: string, dataBase64?: string }[]} images
66
+ * @param {{ colorizeImages: boolean }} options Normalized render options.
67
+ * @returns {string}
68
+ */
69
+ static #buildImageFilterMarkup(images, options) {
70
+ if (!options.colorizeImages) return ''
71
+
72
+ const hasDiagramImage = (images || []).some(
73
+ (image) =>
74
+ image?.dataBase64 &&
75
+ image?.mimeType &&
76
+ SchematicImageRenderer.#isDiagramImage(image) &&
77
+ !SchematicImageRenderer.#isPowerDiagramImage(image)
78
+ )
79
+
80
+ if (!hasDiagramImage) return ''
81
+
82
+ return (
83
+ '<defs>' +
84
+ SchematicImageRenderer.#buildBlueprintImageFilterMarkup() +
85
+ '</defs>'
86
+ )
35
87
  }
36
88
 
37
89
  /**
38
90
  * Builds one embedded SVG image node.
39
- * @param {{ x: number, y: number, cornerX: number, cornerY: number, mimeType: string, dataBase64: string, keepAspect?: boolean }} image
91
+ * @param {{ x: number, y: number, cornerX: number, cornerY: number, fileName?: string, mimeType: string, dataBase64: string, keepAspect?: boolean }} image
40
92
  * @param {number} sheetHeight
93
+ * @param {{ colorizeImages: boolean }} options Normalized render options.
41
94
  * @returns {string}
42
95
  */
43
- static #buildEmbeddedImageMarkup(image, sheetHeight) {
96
+ static #buildEmbeddedImageMarkup(image, sheetHeight, options) {
44
97
  const bounds = SchematicImageRenderer.#resolveBounds(image, sheetHeight)
98
+ const isDiagramImage = SchematicImageRenderer.#isDiagramImage(image)
99
+ const isPowerDiagramImage =
100
+ SchematicImageRenderer.#isPowerDiagramImage(image)
101
+ const colorizeImage = options.colorizeImages
102
+ const dataBase64 =
103
+ colorizeImage && isPowerDiagramImage
104
+ ? SchematicPowerDiagramImageProcessor.process(image)
105
+ : image.dataBase64
45
106
 
46
107
  return (
47
- '<image class="schematic-embedded-image" x="' +
108
+ '<image class="' +
109
+ escapeHtml(
110
+ isPowerDiagramImage
111
+ ? 'schematic-embedded-image schematic-embedded-image--power-diagram'
112
+ : isDiagramImage
113
+ ? 'schematic-embedded-image schematic-embedded-image--diagram'
114
+ : 'schematic-embedded-image'
115
+ ) +
116
+ '" x="' +
48
117
  formatNumber(bounds.x) +
49
118
  '" y="' +
50
119
  formatNumber(bounds.y) +
@@ -54,14 +123,60 @@ export class SchematicImageRenderer {
54
123
  formatNumber(bounds.height) +
55
124
  '" preserveAspectRatio="' +
56
125
  escapeHtml(image.keepAspect === false ? 'none' : 'xMidYMid meet') +
57
- '" href="' +
58
- escapeHtml(
59
- 'data:' + image.mimeType + ';base64,' + image.dataBase64
60
- ) +
126
+ '"' +
127
+ (colorizeImage && isDiagramImage && !isPowerDiagramImage
128
+ ? ' filter="url(#' +
129
+ escapeHtml(BLUEPRINT_IMAGE_FILTER_ID) +
130
+ ')"'
131
+ : '') +
132
+ ' href="' +
133
+ escapeHtml('data:' + image.mimeType + ';base64,' + dataBase64) +
61
134
  '" />'
62
135
  )
63
136
  }
64
137
 
138
+ /**
139
+ * Builds a filter that maps black diagram artwork to the schematic blue
140
+ * ink while keeping white backgrounds white.
141
+ * @returns {string}
142
+ */
143
+ static #buildBlueprintImageFilterMarkup() {
144
+ return (
145
+ '<filter id="' +
146
+ escapeHtml(BLUEPRINT_IMAGE_FILTER_ID) +
147
+ '" color-interpolation-filters="sRGB">' +
148
+ '<feColorMatrix type="matrix" values="' +
149
+ '1 0 0 0 0 ' +
150
+ '0 0.431 0 0 0.569 ' +
151
+ '0 0 0.325 0 0.675 ' +
152
+ '0 0 0 1 0' +
153
+ '" />' +
154
+ '</filter>'
155
+ )
156
+ }
157
+
158
+ /**
159
+ * Identifies recovered diagram artwork by explicit file naming.
160
+ * @param {{ fileName?: string, mimeType?: string }} image
161
+ * @returns {boolean}
162
+ */
163
+ static #isDiagramImage(image) {
164
+ return /diagram/i.test(String(image?.fileName || ''))
165
+ }
166
+
167
+ /**
168
+ * Identifies recovered power-diagram PNG artwork for selective palette
169
+ * processing.
170
+ * @param {{ fileName?: string, mimeType?: string }} image
171
+ * @returns {boolean}
172
+ */
173
+ static #isPowerDiagramImage(image) {
174
+ return (
175
+ image?.mimeType === 'image/png' &&
176
+ /power/i.test(String(image?.fileName || ''))
177
+ )
178
+ }
179
+
65
180
  /**
66
181
  * Builds one placeholder frame when an image payload is unavailable.
67
182
  * @param {{ x: number, y: number, cornerX: number, cornerY: number, fileName?: string }} image
@@ -44,7 +44,7 @@ export class SchematicJunctionRenderer {
44
44
  formatNumber(projectSchematicY(sheetHeight, junction.y)) +
45
45
  '" r="2" fill="' +
46
46
  escapeHtml(
47
- SchematicColorResolver.resolveColor(
47
+ SchematicColorResolver.resolveNonTextColor(
48
48
  junction.color,
49
49
  '--schematic-default-ink-color'
50
50
  )
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicColorResolver } from './SchematicColorResolver.mjs'
6
+
7
+ /**
8
+ * Resolves schematic line stroke colors from normalized line metadata.
9
+ */
10
+ export class SchematicLineColorResolver {
11
+ /**
12
+ * Resolves one schematic line stroke color.
13
+ * @param {{ color?: string, ownerIndex?: string, isBus?: boolean, recordType?: string, x1?: number, x2?: number }} line Line primitive.
14
+ * @returns {string}
15
+ */
16
+ static resolveColor(line) {
17
+ if (SchematicLineColorResolver.isElectricalLine(line)) {
18
+ return SchematicColorResolver.resolveNonTextColor(
19
+ line?.color,
20
+ '--schematic-default-ink-color'
21
+ )
22
+ }
23
+
24
+ if (SchematicLineColorResolver.#isOwnerVerticalLine(line)) {
25
+ const resolvedColor = SchematicColorResolver.resolveNonTextColor(
26
+ line?.color,
27
+ '--schematic-default-ink-color',
28
+ true
29
+ )
30
+ return resolvedColor.startsWith('var(')
31
+ ? resolvedColor
32
+ : SchematicColorResolver.resolveMutedSourceColor(
33
+ line?.color,
34
+ '--schematic-default-ink-color'
35
+ )
36
+ }
37
+
38
+ return SchematicColorResolver.resolveColor(
39
+ line?.color,
40
+ '--schematic-default-ink-color'
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Returns true when one normalized line can carry schematic net color.
46
+ * @param {{ ownerIndex?: string, isBus?: boolean, recordType?: string } | null | undefined} line Line primitive.
47
+ * @returns {boolean}
48
+ */
49
+ static isElectricalLine(line) {
50
+ if (line?.ownerIndex || line?.isBus === true) {
51
+ return false
52
+ }
53
+
54
+ if (!Object.prototype.hasOwnProperty.call(line || {}, 'recordType')) {
55
+ return true
56
+ }
57
+
58
+ return !['6', '7', '26'].includes(String(line.recordType || ''))
59
+ }
60
+
61
+ /**
62
+ * Returns true when owner geometry encodes a vertical side rail as a line.
63
+ * @param {{ ownerIndex?: string, x1?: number, x2?: number } | null | undefined} line Line primitive.
64
+ * @returns {boolean}
65
+ */
66
+ static #isOwnerVerticalLine(line) {
67
+ return (
68
+ Boolean(String(line?.ownerIndex || '').trim()) &&
69
+ SchematicLineColorResolver.#isVerticalLine(line)
70
+ )
71
+ }
72
+
73
+ /**
74
+ * Returns true when one line is vertical within parser coordinate tolerance.
75
+ * @param {{ x1?: number, x2?: number } | null | undefined} line Line primitive.
76
+ * @returns {boolean}
77
+ */
78
+ static #isVerticalLine(line) {
79
+ const x1 = Number(line?.x1)
80
+ const x2 = Number(line?.x2)
81
+
82
+ return (
83
+ Number.isFinite(x1) &&
84
+ Number.isFinite(x2) &&
85
+ Math.abs(x1 - x2) <= 0.001
86
+ )
87
+ }
88
+ }
@@ -0,0 +1,275 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Splits native Altium footer/template primitives from scalable schematic
7
+ * content when a standard template page is larger than the stored custom size.
8
+ */
9
+ export class SchematicNativeFooterPartitioner {
10
+ /**
11
+ * Partitions render primitives into scalable content and fixed footer
12
+ * chrome.
13
+ * @param {{ lines?: object[], polygons?: object[], rectangles?: object[], roundedRectangles?: object[], ellipses?: object[], arcs?: object[], beziers?: object[], pies?: object[], ieeeSymbols?: object[], texts?: object[], images?: object[] }} primitives Primitive families.
14
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
15
+ * @returns {{ content: Record<string, object[]>, footer: Record<string, object[]>, footerBounds: { minX: number, minY: number, maxX: number, maxY: number } | null }}
16
+ */
17
+ static partitionPrimitives(primitives, sheet) {
18
+ const families = [
19
+ 'lines',
20
+ 'polygons',
21
+ 'rectangles',
22
+ 'roundedRectangles',
23
+ 'ellipses',
24
+ 'arcs',
25
+ 'beziers',
26
+ 'pies',
27
+ 'ieeeSymbols',
28
+ 'texts',
29
+ 'images'
30
+ ]
31
+ const content = {}
32
+ const footer = {}
33
+
34
+ for (const family of families) {
35
+ const values = primitives?.[family] || []
36
+ content[family] = values.filter(
37
+ (primitive) =>
38
+ !SchematicNativeFooterPartitioner.#isNativeFooterPrimitive(
39
+ primitive,
40
+ sheet
41
+ )
42
+ )
43
+ footer[family] = values.filter((primitive) =>
44
+ SchematicNativeFooterPartitioner.#isNativeFooterPrimitive(
45
+ primitive,
46
+ sheet
47
+ )
48
+ )
49
+ }
50
+
51
+ return {
52
+ content,
53
+ footer,
54
+ footerBounds:
55
+ SchematicNativeFooterPartitioner.#resolvePrimitiveSetBounds(
56
+ Object.values(footer).flat()
57
+ )
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Returns true when one primitive is native lower-page footer chrome.
63
+ * @param {object} primitive Primitive candidate.
64
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
65
+ * @returns {boolean}
66
+ */
67
+ static #isNativeFooterPrimitive(primitive, sheet) {
68
+ if (!SchematicNativeFooterPartitioner.#shouldSplitNativeFooter(sheet)) {
69
+ return false
70
+ }
71
+ if (!String(primitive?.ownerIndex || '').trim()) {
72
+ return false
73
+ }
74
+
75
+ const bounds =
76
+ SchematicNativeFooterPartitioner.#resolvePrimitiveBounds(primitive)
77
+ if (!bounds) {
78
+ return false
79
+ }
80
+
81
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
82
+ const footerLimit = Math.max(margin * 6, 120)
83
+ const footerStartX = Math.max(Number(sheet?.width || 0), 0) * 0.5
84
+
85
+ return bounds.maxY <= footerLimit && bounds.maxX >= footerStartX
86
+ }
87
+
88
+ /**
89
+ * Returns true when promoted standard-sheet content needs fixed native
90
+ * footer handling.
91
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
92
+ * @returns {boolean}
93
+ */
94
+ static #shouldSplitNativeFooter(sheet) {
95
+ const width = Number(sheet?.width || 0)
96
+ const height = Number(sheet?.height || 0)
97
+ const sourceWidth = Number(sheet?.sourceWidth || 0)
98
+ const sourceHeight = Number(sheet?.sourceHeight || 0)
99
+
100
+ return Boolean(
101
+ sheet?.borderOn &&
102
+ sheet?.paperSize &&
103
+ sourceWidth > 0 &&
104
+ sourceHeight > 0 &&
105
+ (width > sourceWidth || height > sourceHeight)
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Resolves one primitive's document-space bounds.
111
+ * @param {object} primitive Primitive candidate.
112
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
113
+ */
114
+ static #resolvePrimitiveBounds(primitive) {
115
+ const points =
116
+ SchematicNativeFooterPartitioner.#collectPrimitivePoints(primitive)
117
+ if (!points.length) {
118
+ return null
119
+ }
120
+
121
+ return {
122
+ minX: Math.min(...points.map((point) => point.x)),
123
+ minY: Math.min(...points.map((point) => point.y)),
124
+ maxX: Math.max(...points.map((point) => point.x)),
125
+ maxY: Math.max(...points.map((point) => point.y))
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Resolves combined bounds for a set of primitives.
131
+ * @param {object[]} primitives Primitive candidates.
132
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
133
+ */
134
+ static #resolvePrimitiveSetBounds(primitives) {
135
+ const bounds = primitives
136
+ .map((primitive) =>
137
+ SchematicNativeFooterPartitioner.#resolvePrimitiveBounds(
138
+ primitive
139
+ )
140
+ )
141
+ .filter(Boolean)
142
+
143
+ if (!bounds.length) {
144
+ return null
145
+ }
146
+
147
+ return {
148
+ minX: Math.min(...bounds.map((bound) => bound.minX)),
149
+ minY: Math.min(...bounds.map((bound) => bound.minY)),
150
+ maxX: Math.max(...bounds.map((bound) => bound.maxX)),
151
+ maxY: Math.max(...bounds.map((bound) => bound.maxY))
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Collects point coordinates from one normalized primitive.
157
+ * @param {object} primitive Primitive candidate.
158
+ * @returns {{ x: number, y: number }[]}
159
+ */
160
+ static #collectPrimitivePoints(primitive) {
161
+ if (Array.isArray(primitive?.points)) {
162
+ return primitive.points
163
+ .map((point) =>
164
+ SchematicNativeFooterPartitioner.#point(point?.x, point?.y)
165
+ )
166
+ .filter(Boolean)
167
+ }
168
+ if (Array.isArray(primitive?.segments)) {
169
+ return primitive.segments.flatMap((segment) =>
170
+ [
171
+ SchematicNativeFooterPartitioner.#point(
172
+ segment?.x1,
173
+ segment?.y1
174
+ ),
175
+ SchematicNativeFooterPartitioner.#point(
176
+ segment?.x2,
177
+ segment?.y2
178
+ )
179
+ ].filter(Boolean)
180
+ )
181
+ }
182
+ if (
183
+ Number.isFinite(Number(primitive?.x1)) &&
184
+ Number.isFinite(Number(primitive?.y1)) &&
185
+ Number.isFinite(Number(primitive?.x2)) &&
186
+ Number.isFinite(Number(primitive?.y2))
187
+ ) {
188
+ return [
189
+ SchematicNativeFooterPartitioner.#point(
190
+ primitive.x1,
191
+ primitive.y1
192
+ ),
193
+ SchematicNativeFooterPartitioner.#point(
194
+ primitive.x2,
195
+ primitive.y2
196
+ )
197
+ ]
198
+ }
199
+ if (
200
+ Number.isFinite(Number(primitive?.cornerX)) &&
201
+ Number.isFinite(Number(primitive?.cornerY))
202
+ ) {
203
+ return [
204
+ SchematicNativeFooterPartitioner.#point(
205
+ primitive.x,
206
+ primitive.y
207
+ ),
208
+ SchematicNativeFooterPartitioner.#point(
209
+ primitive.cornerX,
210
+ primitive.cornerY
211
+ )
212
+ ].filter(Boolean)
213
+ }
214
+ if (
215
+ Number.isFinite(Number(primitive?.width)) &&
216
+ Number.isFinite(Number(primitive?.height))
217
+ ) {
218
+ return [
219
+ SchematicNativeFooterPartitioner.#point(
220
+ primitive.x,
221
+ primitive.y
222
+ ),
223
+ SchematicNativeFooterPartitioner.#point(
224
+ Number(primitive.x || 0) + Number(primitive.width || 0),
225
+ Number(primitive.y || 0) + Number(primitive.height || 0)
226
+ )
227
+ ].filter(Boolean)
228
+ }
229
+ if (
230
+ Number.isFinite(Number(primitive?.radius)) ||
231
+ Number.isFinite(Number(primitive?.radiusX))
232
+ ) {
233
+ const radiusX = Math.max(
234
+ Number(primitive?.radiusX || primitive?.radius || 0),
235
+ 0
236
+ )
237
+ const radiusY = Math.max(
238
+ Number(primitive?.radiusY || primitive?.radius || 0),
239
+ 0
240
+ )
241
+
242
+ return [
243
+ SchematicNativeFooterPartitioner.#point(
244
+ Number(primitive.x || 0) - radiusX,
245
+ Number(primitive.y || 0) - radiusY
246
+ ),
247
+ SchematicNativeFooterPartitioner.#point(
248
+ Number(primitive.x || 0) + radiusX,
249
+ Number(primitive.y || 0) + radiusY
250
+ )
251
+ ]
252
+ }
253
+
254
+ return [
255
+ SchematicNativeFooterPartitioner.#point(primitive?.x, primitive?.y)
256
+ ].filter(Boolean)
257
+ }
258
+
259
+ /**
260
+ * Builds one finite point.
261
+ * @param {number | string | undefined} x Raw x coordinate.
262
+ * @param {number | string | undefined} y Raw y coordinate.
263
+ * @returns {{ x: number, y: number } | null}
264
+ */
265
+ static #point(x, y) {
266
+ const normalizedX = Number(x)
267
+ const normalizedY = Number(y)
268
+
269
+ if (!Number.isFinite(normalizedX) || !Number.isFinite(normalizedY)) {
270
+ return null
271
+ }
272
+
273
+ return { x: normalizedX, y: normalizedY }
274
+ }
275
+ }