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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  3. package/src/core/altium/AltiumParser.mjs +240 -8
  4. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  5. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  6. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  7. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  8. package/src/core/altium/SchematicImageParser.mjs +291 -6
  9. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  10. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  11. package/src/core/altium/SchematicPinParser.mjs +175 -4
  12. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  13. package/src/core/altium/SchematicTextParser.mjs +125 -11
  14. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  15. package/src/ui/SchematicColorResolver.mjs +78 -0
  16. package/src/ui/SchematicContentLayout.mjs +58 -1
  17. package/src/ui/SchematicImageRenderer.mjs +125 -10
  18. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  19. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  20. package/src/ui/SchematicNoteRenderer.mjs +82 -6
  21. package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
  22. package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
  23. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  24. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  25. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  26. package/src/ui/SchematicShapeRenderer.mjs +82 -23
  27. 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
- * Adds multipart section suffixes like A/B/J to visible designator texts
51
- * when the active Altium part id is stored separately from the base
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
- const duplicatedActiveBaseDesignators =
59
- SchematicTextPostProcessor.#collectDuplicatedActiveMultipartDesignators(
60
- texts,
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 }[]} rectangles
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
- rectangles = []
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 = SchematicTextPostProcessor.#buildOwnerBounds(
154
- lines,
155
- pins,
156
- rectangles
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([text.x, projectSchematicY(sheetHeight, text.y)])
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
- 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
  )