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.
- package/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- 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
|
-
|
|
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="
|
|
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
|
-
'"
|
|
58
|
-
|
|
59
|
-
'
|
|
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.
|
|
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
|
+
}
|