altium-toolkit 1.1.1 → 1.1.3
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/package.json +1 -1
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumParser.mjs +240 -8
- package/src/core/altium/NaturalStringComparator.mjs +175 -0
- package/src/core/altium/ParserUtils.mjs +83 -14
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +104 -33
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +26 -4
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +28 -5
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +1 -3
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +59 -28
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +66 -29
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +50 -16
- 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/SchematicDisplayModeCatalogParser.mjs +36 -11
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicImplementationParser.mjs +75 -36
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicPinParser.mjs +175 -4
- package/src/core/altium/SchematicQaReportBuilder.mjs +115 -38
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/ui/PcbInteractionIndex.mjs +16 -1
- package/src/ui/SchematicColorResolver.mjs +78 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +82 -6
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
- package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
- 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 +82 -23
- package/src/ui/SchematicSvgRenderer.mjs +293 -47
|
@@ -721,7 +721,12 @@ export class SchematicContentLayout {
|
|
|
721
721
|
}
|
|
722
722
|
|
|
723
723
|
for (const text of schematic?.texts || []) {
|
|
724
|
-
coordinates.push(
|
|
724
|
+
coordinates.push(
|
|
725
|
+
...SchematicContentLayout.#collectRenderedTextPoints(
|
|
726
|
+
text,
|
|
727
|
+
sheetHeight
|
|
728
|
+
)
|
|
729
|
+
)
|
|
725
730
|
}
|
|
726
731
|
|
|
727
732
|
for (const component of schematic?.components || []) {
|
|
@@ -829,6 +834,58 @@ export class SchematicContentLayout {
|
|
|
829
834
|
}))
|
|
830
835
|
}
|
|
831
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Collects rendered bounds points for a text primitive.
|
|
839
|
+
* @param {{ x?: number, y?: number, recordType?: string, cornerX?: number, cornerY?: number, noteLines?: string[] }} text Text primitive.
|
|
840
|
+
* @param {number} sheetHeight Sheet height.
|
|
841
|
+
* @returns {number[][]}
|
|
842
|
+
*/
|
|
843
|
+
static #collectRenderedTextPoints(text, sheetHeight) {
|
|
844
|
+
const x = Number(text?.x)
|
|
845
|
+
const y = Number(text?.y)
|
|
846
|
+
|
|
847
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
848
|
+
return []
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (SchematicContentLayout.#hasRenderedTextFrame(text)) {
|
|
852
|
+
const cornerX = Number(text.cornerX)
|
|
853
|
+
const cornerY = Number(text.cornerY)
|
|
854
|
+
const left = Math.min(x, cornerX)
|
|
855
|
+
const right = Math.max(x, cornerX)
|
|
856
|
+
const top = Math.min(
|
|
857
|
+
projectSchematicY(sheetHeight, y),
|
|
858
|
+
projectSchematicY(sheetHeight, cornerY)
|
|
859
|
+
)
|
|
860
|
+
const bottom = Math.max(
|
|
861
|
+
projectSchematicY(sheetHeight, y),
|
|
862
|
+
projectSchematicY(sheetHeight, cornerY)
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
return [
|
|
866
|
+
[left, top],
|
|
867
|
+
[right, bottom]
|
|
868
|
+
]
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return [[x, projectSchematicY(sheetHeight, y)]]
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Returns true when a text primitive renders as a framed note/callout box.
|
|
876
|
+
* @param {{ recordType?: string, cornerX?: number, cornerY?: number, noteLines?: string[] }} text Text primitive.
|
|
877
|
+
* @returns {boolean}
|
|
878
|
+
*/
|
|
879
|
+
static #hasRenderedTextFrame(text) {
|
|
880
|
+
const recordType = String(text?.recordType || '')
|
|
881
|
+
|
|
882
|
+
return Boolean(
|
|
883
|
+
(recordType === '28' || recordType === '209') &&
|
|
884
|
+
Number.isFinite(Number(text?.cornerX)) &&
|
|
885
|
+
Number.isFinite(Number(text?.cornerY))
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
832
889
|
/**
|
|
833
890
|
* Computes the inner endpoint for one schematic pin stub.
|
|
834
891
|
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
@@ -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,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
|
+
}
|
|
@@ -15,7 +15,7 @@ const MINIMUM_NOTE_TEXT_SIZE = 4
|
|
|
15
15
|
export class SchematicNoteRenderer {
|
|
16
16
|
/**
|
|
17
17
|
* Builds one boxed schematic note/callout with wrapped text rows.
|
|
18
|
-
* @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, lineWidth?: number, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
|
|
18
|
+
* @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, anchor?: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, lineWidth?: number, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
|
|
19
19
|
* @param {number} sheetHeight
|
|
20
20
|
* @returns {string}
|
|
21
21
|
*/
|
|
@@ -59,6 +59,10 @@ export class SchematicNoteRenderer {
|
|
|
59
59
|
height,
|
|
60
60
|
requestedTextSize
|
|
61
61
|
)
|
|
62
|
+
const compactMarkerNote = SchematicNoteRenderer.#isCompactMarkerNote(
|
|
63
|
+
noteSourceLines,
|
|
64
|
+
compactSingleLineNote
|
|
65
|
+
)
|
|
62
66
|
const horizontalTextMargin =
|
|
63
67
|
SchematicNoteRenderer.#resolveHorizontalTextMargin(
|
|
64
68
|
textMargin,
|
|
@@ -94,6 +98,7 @@ export class SchematicNoteRenderer {
|
|
|
94
98
|
lineHeight,
|
|
95
99
|
textSize,
|
|
96
100
|
compactSingleLineNote,
|
|
101
|
+
compactMarkerNote,
|
|
97
102
|
text
|
|
98
103
|
)
|
|
99
104
|
)
|
|
@@ -462,7 +467,8 @@ export class SchematicNoteRenderer {
|
|
|
462
467
|
* @param {number} lineHeight
|
|
463
468
|
* @param {number} textSize
|
|
464
469
|
* @param {boolean} compactSingleLineNote
|
|
465
|
-
* @param {
|
|
470
|
+
* @param {boolean} compactMarkerNote
|
|
471
|
+
* @param {{ color: string, fontFamily?: string, fontWeight?: number, fontStyle?: string, anchor?: 'start' | 'middle' | 'end' }} text
|
|
466
472
|
* @returns {string}
|
|
467
473
|
*/
|
|
468
474
|
static #buildNoteLineMarkup(
|
|
@@ -476,9 +482,21 @@ export class SchematicNoteRenderer {
|
|
|
476
482
|
lineHeight,
|
|
477
483
|
textSize,
|
|
478
484
|
compactSingleLineNote,
|
|
485
|
+
compactMarkerNote,
|
|
479
486
|
text
|
|
480
487
|
) {
|
|
481
|
-
const
|
|
488
|
+
const anchor = SchematicNoteRenderer.#resolveTextAnchor(
|
|
489
|
+
text,
|
|
490
|
+
compactMarkerNote
|
|
491
|
+
)
|
|
492
|
+
const x = SchematicNoteRenderer.#resolveLineX(
|
|
493
|
+
left,
|
|
494
|
+
right,
|
|
495
|
+
horizontalTextMargin,
|
|
496
|
+
anchor
|
|
497
|
+
)
|
|
498
|
+
const ruleLeft = left + horizontalTextMargin
|
|
499
|
+
const ruleRight = right - horizontalTextMargin
|
|
482
500
|
const y =
|
|
483
501
|
top +
|
|
484
502
|
verticalTextMargin +
|
|
@@ -491,11 +509,11 @@ export class SchematicNoteRenderer {
|
|
|
491
509
|
if (/^_+$/.test(String(line || '').trim())) {
|
|
492
510
|
return (
|
|
493
511
|
'<line class="schematic-note-rule" x1="' +
|
|
494
|
-
formatNumber(
|
|
512
|
+
formatNumber(ruleLeft) +
|
|
495
513
|
'" y1="' +
|
|
496
514
|
formatNumber(y - textSize * 0.35) +
|
|
497
515
|
'" x2="' +
|
|
498
|
-
formatNumber(
|
|
516
|
+
formatNumber(ruleRight) +
|
|
499
517
|
'" y2="' +
|
|
500
518
|
formatNumber(y - textSize * 0.35) +
|
|
501
519
|
'" stroke="' +
|
|
@@ -521,7 +539,9 @@ export class SchematicNoteRenderer {
|
|
|
521
539
|
'--schematic-text-color'
|
|
522
540
|
)
|
|
523
541
|
) +
|
|
524
|
-
'" text-anchor="
|
|
542
|
+
'" text-anchor="' +
|
|
543
|
+
escapeHtml(anchor) +
|
|
544
|
+
'" font-size="' +
|
|
525
545
|
formatNumber(textSize) +
|
|
526
546
|
'" font-family="' +
|
|
527
547
|
escapeHtml(text.fontFamily || 'Times New Roman') +
|
|
@@ -534,6 +554,62 @@ export class SchematicNoteRenderer {
|
|
|
534
554
|
)
|
|
535
555
|
}
|
|
536
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Resolves the x coordinate for a note line from its text anchor.
|
|
559
|
+
* @param {number} left Note box left edge.
|
|
560
|
+
* @param {number} right Note box right edge.
|
|
561
|
+
* @param {number} horizontalTextMargin Text margin.
|
|
562
|
+
* @param {'start' | 'middle' | 'end'} anchor Text anchor.
|
|
563
|
+
* @returns {number}
|
|
564
|
+
*/
|
|
565
|
+
static #resolveLineX(left, right, horizontalTextMargin, anchor) {
|
|
566
|
+
if (anchor === 'middle') return (left + right) / 2
|
|
567
|
+
if (anchor === 'end') return right - horizontalTextMargin
|
|
568
|
+
|
|
569
|
+
return left + horizontalTextMargin
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Returns true for tight single-token marker boxes that Altium centers
|
|
574
|
+
* inside the authored note rectangle.
|
|
575
|
+
* @param {string[]} noteLines Source note rows.
|
|
576
|
+
* @param {boolean} compactSingleLineNote True for tight one-line note boxes.
|
|
577
|
+
* @returns {boolean}
|
|
578
|
+
*/
|
|
579
|
+
static #isCompactMarkerNote(noteLines, compactSingleLineNote) {
|
|
580
|
+
if (!compactSingleLineNote) {
|
|
581
|
+
return false
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const visibleLines = noteLines
|
|
585
|
+
.map((line) => String(line || '').trim())
|
|
586
|
+
.filter(Boolean)
|
|
587
|
+
|
|
588
|
+
if (visibleLines.length !== 1) {
|
|
589
|
+
return false
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return /^[A-Z0-9._/-]{1,4}$/u.test(visibleLines[0])
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Normalizes note text anchors to SVG text-anchor values.
|
|
597
|
+
* @param {{ anchor?: string }} text Note text.
|
|
598
|
+
* @param {boolean} compactMarkerNote True for centered marker notes.
|
|
599
|
+
* @returns {'start' | 'middle' | 'end'}
|
|
600
|
+
*/
|
|
601
|
+
static #resolveTextAnchor(text, compactMarkerNote) {
|
|
602
|
+
if (compactMarkerNote) {
|
|
603
|
+
return 'middle'
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (text.anchor === 'middle' || text.anchor === 'end') {
|
|
607
|
+
return text.anchor
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return 'start'
|
|
611
|
+
}
|
|
612
|
+
|
|
537
613
|
/**
|
|
538
614
|
* Builds an optional note font-style attribute.
|
|
539
615
|
* @param {string | undefined} fontStyle
|