altium-toolkit 1.1.2 → 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/PcbEmbeddedFontExtractor.mjs +186 -43
- 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/SchematicImageParser.mjs +291 -6
- 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/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- 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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { SchematicMultipartDesignatorNormalizer } from './SchematicMultipartDesignatorNormalizer.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Applies placement-oriented cleanup passes to normalized schematic text.
|
|
7
9
|
*/
|
|
@@ -47,84 +49,17 @@ export class SchematicTextPostProcessor {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
51
|
-
* when the active Altium part id is stored separately from the
|
|
52
|
+
* Normalizes multipart section suffixes like A/B/J on visible designator
|
|
53
|
+
* texts when the active Altium part id is stored separately from the
|
|
52
54
|
* designator string.
|
|
53
55
|
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
54
56
|
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
55
57
|
* @returns {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]}
|
|
56
58
|
*/
|
|
57
59
|
static decorateMultipartDesignators(texts, activeMultipartOwnerParts) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
activeMultipartOwnerParts
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return texts.map((text) => {
|
|
65
|
-
const ownerIndex = String(text.ownerIndex || '')
|
|
66
|
-
const suffix =
|
|
67
|
-
SchematicTextPostProcessor.#formatMultipartPartSuffix(
|
|
68
|
-
activeMultipartOwnerParts.get(ownerIndex)
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
!suffix ||
|
|
73
|
-
text.recordType !== '34' ||
|
|
74
|
-
String(text.name || '')
|
|
75
|
-
.trim()
|
|
76
|
-
.toLowerCase() !== 'designator' ||
|
|
77
|
-
!duplicatedActiveBaseDesignators.has(text.text)
|
|
78
|
-
) {
|
|
79
|
-
return text
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!/\d$/i.test(text.text) || text.text.endsWith(suffix)) {
|
|
83
|
-
return text
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
...text,
|
|
88
|
-
text: text.text + suffix
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Collects base multipart designators that belong to more than one active
|
|
95
|
-
* visible owner on the current sheet.
|
|
96
|
-
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
97
|
-
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
98
|
-
* @returns {Set<string>}
|
|
99
|
-
*/
|
|
100
|
-
static #collectDuplicatedActiveMultipartDesignators(
|
|
101
|
-
texts,
|
|
102
|
-
activeMultipartOwnerParts
|
|
103
|
-
) {
|
|
104
|
-
const counts = new Map()
|
|
105
|
-
|
|
106
|
-
for (const text of texts) {
|
|
107
|
-
const ownerIndex = String(text.ownerIndex || '')
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
!activeMultipartOwnerParts.has(ownerIndex) ||
|
|
111
|
-
text.recordType !== '34' ||
|
|
112
|
-
String(text.name || '')
|
|
113
|
-
.trim()
|
|
114
|
-
.toLowerCase() !== 'designator' ||
|
|
115
|
-
!/\d$/i.test(String(text.text || '').trim())
|
|
116
|
-
) {
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const baseDesignator = String(text.text || '').trim()
|
|
121
|
-
counts.set(baseDesignator, (counts.get(baseDesignator) || 0) + 1)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return new Set(
|
|
125
|
-
[...counts.entries()]
|
|
126
|
-
.filter(([, count]) => count > 1)
|
|
127
|
-
.map(([baseDesignator]) => baseDesignator)
|
|
60
|
+
return SchematicMultipartDesignatorNormalizer.normalize(
|
|
61
|
+
texts,
|
|
62
|
+
activeMultipartOwnerParts
|
|
128
63
|
)
|
|
129
64
|
}
|
|
130
65
|
|
|
@@ -136,7 +71,7 @@ export class SchematicTextPostProcessor {
|
|
|
136
71
|
* @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
|
|
137
72
|
* @param {{ x: number, y: number, ownerIndex: string, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
138
73
|
* @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} ports
|
|
139
|
-
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[]}
|
|
74
|
+
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
140
75
|
* @returns {{ x: number, y: number, text: string, name?: string, ownerIndex?: string, recordType?: string, rotation?: number, anchor?: 'start' | 'middle' | 'end' }[]}
|
|
141
76
|
*/
|
|
142
77
|
static anchorComponentTextsFromOwnerBounds(
|
|
@@ -144,17 +79,22 @@ export class SchematicTextPostProcessor {
|
|
|
144
79
|
lines,
|
|
145
80
|
pins,
|
|
146
81
|
ports = [],
|
|
147
|
-
|
|
82
|
+
bodyPrimitives = []
|
|
148
83
|
) {
|
|
84
|
+
const normalizedBodyPrimitives =
|
|
85
|
+
SchematicTextPostProcessor.#normalizeOwnerBodyPrimitives(
|
|
86
|
+
bodyPrimitives
|
|
87
|
+
)
|
|
149
88
|
const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
|
|
150
89
|
lines,
|
|
151
90
|
pins
|
|
152
91
|
)
|
|
153
|
-
const ownerBodyBounds =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
92
|
+
const ownerBodyBounds =
|
|
93
|
+
SchematicTextPostProcessor.#buildOwnerBodyBounds(
|
|
94
|
+
lines,
|
|
95
|
+
pins,
|
|
96
|
+
normalizedBodyPrimitives
|
|
97
|
+
)
|
|
158
98
|
const ownerPinCounts =
|
|
159
99
|
SchematicTextPostProcessor.#buildOwnerPinCounts(pins)
|
|
160
100
|
const ownerPinOrientations =
|
|
@@ -236,6 +176,131 @@ export class SchematicTextPostProcessor {
|
|
|
236
176
|
})
|
|
237
177
|
}
|
|
238
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Normalizes owner body primitive arguments while preserving the older
|
|
181
|
+
* rectangle-array call shape.
|
|
182
|
+
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
183
|
+
* @returns {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }}
|
|
184
|
+
*/
|
|
185
|
+
static #normalizeOwnerBodyPrimitives(bodyPrimitives) {
|
|
186
|
+
if (Array.isArray(bodyPrimitives)) {
|
|
187
|
+
return {
|
|
188
|
+
rectangles: bodyPrimitives,
|
|
189
|
+
roundedRectangles: [],
|
|
190
|
+
ellipses: [],
|
|
191
|
+
arcs: [],
|
|
192
|
+
pies: []
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
rectangles: bodyPrimitives?.rectangles || [],
|
|
198
|
+
roundedRectangles: bodyPrimitives?.roundedRectangles || [],
|
|
199
|
+
ellipses: bodyPrimitives?.ellipses || [],
|
|
200
|
+
arcs: bodyPrimitives?.arcs || [],
|
|
201
|
+
pies: bodyPrimitives?.pies || []
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds per-owner body bounds from straight and curved owner primitives.
|
|
207
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
|
|
208
|
+
* @param {{ x: number, y: number, ownerIndex: string }[]} pins
|
|
209
|
+
* @param {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
210
|
+
* @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
|
|
211
|
+
*/
|
|
212
|
+
static #buildOwnerBodyBounds(lines, pins, bodyPrimitives) {
|
|
213
|
+
const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
|
|
214
|
+
lines,
|
|
215
|
+
pins,
|
|
216
|
+
[...bodyPrimitives.rectangles, ...bodyPrimitives.roundedRectangles]
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
220
|
+
ownerBounds,
|
|
221
|
+
bodyPrimitives.ellipses
|
|
222
|
+
)
|
|
223
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
224
|
+
ownerBounds,
|
|
225
|
+
bodyPrimitives.arcs
|
|
226
|
+
)
|
|
227
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
228
|
+
ownerBounds,
|
|
229
|
+
bodyPrimitives.pies
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return ownerBounds
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Extends owner bounds by primitives expressed as a center and radii.
|
|
237
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds
|
|
238
|
+
* @param {{ x: number, y: number, radius?: number, radiusX?: number, radiusY?: number, ownerIndex?: string }[]} primitives
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
static #extendCenteredOwnerBounds(ownerBounds, primitives) {
|
|
242
|
+
for (const primitive of primitives) {
|
|
243
|
+
if (!primitive.ownerIndex) {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const radiusX =
|
|
248
|
+
SchematicTextPostProcessor.#resolveHorizontalRadius(primitive)
|
|
249
|
+
const radiusY =
|
|
250
|
+
SchematicTextPostProcessor.#resolveVerticalRadius(primitive)
|
|
251
|
+
|
|
252
|
+
if (radiusX <= 0 || radiusY <= 0) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
SchematicTextPostProcessor.#extendBounds(
|
|
257
|
+
ownerBounds,
|
|
258
|
+
primitive.ownerIndex,
|
|
259
|
+
[
|
|
260
|
+
{ x: primitive.x - radiusX, y: primitive.y - radiusY },
|
|
261
|
+
{ x: primitive.x + radiusX, y: primitive.y + radiusY }
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves the horizontal radius of a center-defined primitive.
|
|
269
|
+
* @param {{ radius?: number, radiusX?: number }} primitive
|
|
270
|
+
* @returns {number}
|
|
271
|
+
*/
|
|
272
|
+
static #resolveHorizontalRadius(primitive) {
|
|
273
|
+
return SchematicTextPostProcessor.#coercePositiveNumber(
|
|
274
|
+
primitive.radiusX ?? primitive.radius
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resolves the vertical radius of a center-defined primitive.
|
|
280
|
+
* @param {{ radius?: number, radiusY?: number }} primitive
|
|
281
|
+
* @returns {number}
|
|
282
|
+
*/
|
|
283
|
+
static #resolveVerticalRadius(primitive) {
|
|
284
|
+
return SchematicTextPostProcessor.#coercePositiveNumber(
|
|
285
|
+
primitive.radiusY ?? primitive.radius
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Converts finite positive numeric values, preserving zero for invalids.
|
|
291
|
+
* @param {number | string | undefined | null} value
|
|
292
|
+
* @returns {number}
|
|
293
|
+
*/
|
|
294
|
+
static #coercePositiveNumber(value) {
|
|
295
|
+
const numericValue = Number(value)
|
|
296
|
+
|
|
297
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return numericValue
|
|
302
|
+
}
|
|
303
|
+
|
|
239
304
|
/**
|
|
240
305
|
* Right-aligns wire labels that precede a same-row component designator so
|
|
241
306
|
* they stay clear of the symbol body.
|
|
@@ -401,6 +466,8 @@ export class SchematicTextPostProcessor {
|
|
|
401
466
|
case 2:
|
|
402
467
|
case 33:
|
|
403
468
|
return 8
|
|
469
|
+
case 6:
|
|
470
|
+
return 12
|
|
404
471
|
default:
|
|
405
472
|
return 2
|
|
406
473
|
}
|
|
@@ -744,29 +811,6 @@ export class SchematicTextPostProcessor {
|
|
|
744
811
|
)
|
|
745
812
|
}
|
|
746
813
|
|
|
747
|
-
/**
|
|
748
|
-
* Converts one numeric multipart section id into an alphabetic suffix.
|
|
749
|
-
* @param {string | undefined} partId
|
|
750
|
-
* @returns {string}
|
|
751
|
-
*/
|
|
752
|
-
static #formatMultipartPartSuffix(partId) {
|
|
753
|
-
const numericPartId = Number.parseInt(String(partId || ''), 10)
|
|
754
|
-
if (!Number.isInteger(numericPartId) || numericPartId <= 0) {
|
|
755
|
-
return ''
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let suffix = ''
|
|
759
|
-
let remaining = numericPartId
|
|
760
|
-
|
|
761
|
-
while (remaining > 0) {
|
|
762
|
-
remaining -= 1
|
|
763
|
-
suffix = String.fromCharCode(65 + (remaining % 26)) + suffix
|
|
764
|
-
remaining = Math.floor(remaining / 26)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return suffix
|
|
768
|
-
}
|
|
769
|
-
|
|
770
814
|
/**
|
|
771
815
|
* Returns true when the horizontal wire segment under the label starts at a
|
|
772
816
|
* pin endpoint, which means the label should continue reading rightward.
|
|
@@ -59,6 +59,31 @@ export class SchematicColorResolver {
|
|
|
59
59
|
: SchematicColorResolver.#toVariable(fallbackVariable)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Resolves one non-text primitive color, using the semantic fallback when
|
|
64
|
+
* source artwork only supplies the default black text color.
|
|
65
|
+
* @param {string | undefined} color
|
|
66
|
+
* @param {string} fallbackVariable
|
|
67
|
+
* @param {boolean} [preserveUnknown]
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
static resolveNonTextColor(
|
|
71
|
+
color,
|
|
72
|
+
fallbackVariable,
|
|
73
|
+
preserveUnknown = false
|
|
74
|
+
) {
|
|
75
|
+
const resolved = SchematicColorResolver.resolveColor(
|
|
76
|
+
color,
|
|
77
|
+
fallbackVariable,
|
|
78
|
+
preserveUnknown
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return resolved ===
|
|
82
|
+
SchematicColorResolver.#toVariable('--schematic-text-color')
|
|
83
|
+
? SchematicColorResolver.#toVariable(fallbackVariable)
|
|
84
|
+
: resolved
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
/**
|
|
63
88
|
* Resolves one SVG fill value to a schematic theme variable.
|
|
64
89
|
* @param {string | undefined} fill
|
|
@@ -100,6 +125,59 @@ export class SchematicColorResolver {
|
|
|
100
125
|
: SchematicColorResolver.#toVariable(fallbackVariable)
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Resolves an explicitly authored source stroke color without mapping it to
|
|
130
|
+
* a semantic theme token.
|
|
131
|
+
* @param {string | undefined} color
|
|
132
|
+
* @param {string} fallbackVariable
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
static resolveSourceColor(color, fallbackVariable) {
|
|
136
|
+
return SchematicColorResolver.#resolveSourcePaint(
|
|
137
|
+
color,
|
|
138
|
+
fallbackVariable
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolves an explicitly authored source fill color without mapping it to a
|
|
144
|
+
* semantic theme token.
|
|
145
|
+
* @param {string | undefined} fill
|
|
146
|
+
* @param {string} fallbackVariable
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
static resolveSourceFill(fill, fallbackVariable) {
|
|
150
|
+
return SchematicColorResolver.#resolveSourcePaint(
|
|
151
|
+
fill,
|
|
152
|
+
fallbackVariable
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolves one literal source paint value while preserving SVG control
|
|
158
|
+
* values and falling back only when no source paint exists.
|
|
159
|
+
* @param {string | undefined} paint
|
|
160
|
+
* @param {string} fallbackVariable
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
static #resolveSourcePaint(paint, fallbackVariable) {
|
|
164
|
+
const normalized = SchematicColorResolver.#normalizeColor(paint)
|
|
165
|
+
|
|
166
|
+
if (!normalized) {
|
|
167
|
+
return SchematicColorResolver.#toVariable(fallbackVariable)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
normalized === 'none' ||
|
|
172
|
+
normalized === 'transparent' ||
|
|
173
|
+
normalized.startsWith('var(')
|
|
174
|
+
) {
|
|
175
|
+
return normalized
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return normalized
|
|
179
|
+
}
|
|
180
|
+
|
|
103
181
|
/**
|
|
104
182
|
* Normalizes one raw color string for token lookup.
|
|
105
183
|
* @param {string | undefined} color
|
|
@@ -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
|
)
|