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
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds and cleans bitmap masks for recovered power-diagram line artwork.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicPowerDiagramLineMasks {
|
|
9
|
+
/**
|
|
10
|
+
* Builds a mask for dark linework connected to the detected horizontal
|
|
11
|
+
* rail seeds.
|
|
12
|
+
* @param {{ width: number, height: number }} png PNG data.
|
|
13
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
14
|
+
* @param {Uint8Array} seedMask Initial rail pixels.
|
|
15
|
+
* @returns {Uint8Array}
|
|
16
|
+
*/
|
|
17
|
+
static buildConnectedLineMask(png, darkMask, seedMask) {
|
|
18
|
+
const connectedMask = new Uint8Array(darkMask.length)
|
|
19
|
+
const gapBridgeMask = new Uint8Array(darkMask.length)
|
|
20
|
+
const queue = new Uint32Array(darkMask.length)
|
|
21
|
+
let readIndex = 0
|
|
22
|
+
let writeIndex = 0
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < darkMask.length; index += 1) {
|
|
25
|
+
if (!darkMask[index] || !seedMask[index]) continue
|
|
26
|
+
|
|
27
|
+
connectedMask[index] = 1
|
|
28
|
+
queue[writeIndex] = index
|
|
29
|
+
writeIndex += 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let bridged = true
|
|
33
|
+
|
|
34
|
+
while (bridged) {
|
|
35
|
+
while (readIndex < writeIndex) {
|
|
36
|
+
const index = queue[readIndex]
|
|
37
|
+
readIndex += 1
|
|
38
|
+
|
|
39
|
+
const x = index % png.width
|
|
40
|
+
const y = Math.floor(index / png.width)
|
|
41
|
+
writeIndex =
|
|
42
|
+
SchematicPowerDiagramLineMasks.#appendConnectedNeighbors(
|
|
43
|
+
darkMask,
|
|
44
|
+
connectedMask,
|
|
45
|
+
queue,
|
|
46
|
+
png.width,
|
|
47
|
+
png.height,
|
|
48
|
+
x,
|
|
49
|
+
y,
|
|
50
|
+
writeIndex
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const previousWriteIndex = writeIndex
|
|
55
|
+
writeIndex =
|
|
56
|
+
SchematicPowerDiagramLineMasks.#appendHorizontalGapRuns(
|
|
57
|
+
darkMask,
|
|
58
|
+
connectedMask,
|
|
59
|
+
gapBridgeMask,
|
|
60
|
+
queue,
|
|
61
|
+
png.width,
|
|
62
|
+
png.height,
|
|
63
|
+
writeIndex
|
|
64
|
+
)
|
|
65
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendVerticalGapRuns(
|
|
66
|
+
darkMask,
|
|
67
|
+
connectedMask,
|
|
68
|
+
gapBridgeMask,
|
|
69
|
+
queue,
|
|
70
|
+
png.width,
|
|
71
|
+
png.height,
|
|
72
|
+
writeIndex
|
|
73
|
+
)
|
|
74
|
+
bridged = writeIndex > previousWriteIndex
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return connectedMask
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Appends dark horizontal dash runs that are separated from connected
|
|
82
|
+
* linework only by a small same-row gap.
|
|
83
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
84
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
85
|
+
* @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
|
|
86
|
+
* @param {Uint32Array} queue Shared queue.
|
|
87
|
+
* @param {number} width Image width.
|
|
88
|
+
* @param {number} height Image height.
|
|
89
|
+
* @param {number} writeIndex Queue write index.
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
static #appendHorizontalGapRuns(
|
|
93
|
+
darkMask,
|
|
94
|
+
connectedMask,
|
|
95
|
+
gapBridgeMask,
|
|
96
|
+
queue,
|
|
97
|
+
width,
|
|
98
|
+
height,
|
|
99
|
+
writeIndex
|
|
100
|
+
) {
|
|
101
|
+
const maximumGap = Math.max(8, Math.round(width * 0.015))
|
|
102
|
+
const minimumRun = Math.max(12, Math.round(width * 0.005))
|
|
103
|
+
const maximumRun = Math.max(18, Math.round(width * 0.035))
|
|
104
|
+
|
|
105
|
+
for (let y = 0; y < height; y += 1) {
|
|
106
|
+
const runs = SchematicPowerDiagramLineMasks.#collectHorizontalRuns(
|
|
107
|
+
darkMask,
|
|
108
|
+
connectedMask,
|
|
109
|
+
width,
|
|
110
|
+
y
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
114
|
+
if (!runs[index].connected) continue
|
|
115
|
+
|
|
116
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendGapRun(
|
|
117
|
+
connectedMask,
|
|
118
|
+
gapBridgeMask,
|
|
119
|
+
queue,
|
|
120
|
+
width,
|
|
121
|
+
y,
|
|
122
|
+
runs[index],
|
|
123
|
+
runs[index + 1],
|
|
124
|
+
maximumGap,
|
|
125
|
+
minimumRun,
|
|
126
|
+
maximumRun,
|
|
127
|
+
writeIndex
|
|
128
|
+
)
|
|
129
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendGapRun(
|
|
130
|
+
connectedMask,
|
|
131
|
+
gapBridgeMask,
|
|
132
|
+
queue,
|
|
133
|
+
width,
|
|
134
|
+
y,
|
|
135
|
+
runs[index],
|
|
136
|
+
runs[index - 1],
|
|
137
|
+
maximumGap,
|
|
138
|
+
minimumRun,
|
|
139
|
+
maximumRun,
|
|
140
|
+
writeIndex
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return writeIndex
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Appends dark vertical dash runs that are separated from connected
|
|
150
|
+
* linework only by a small same-column gap.
|
|
151
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
152
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
153
|
+
* @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
|
|
154
|
+
* @param {Uint32Array} queue Shared queue.
|
|
155
|
+
* @param {number} width Image width.
|
|
156
|
+
* @param {number} height Image height.
|
|
157
|
+
* @param {number} writeIndex Queue write index.
|
|
158
|
+
* @returns {number}
|
|
159
|
+
*/
|
|
160
|
+
static #appendVerticalGapRuns(
|
|
161
|
+
darkMask,
|
|
162
|
+
connectedMask,
|
|
163
|
+
gapBridgeMask,
|
|
164
|
+
queue,
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
writeIndex
|
|
168
|
+
) {
|
|
169
|
+
const maximumGap = Math.max(8, Math.round(height * 0.015))
|
|
170
|
+
const minimumRun = Math.max(12, Math.round(height * 0.005))
|
|
171
|
+
const maximumRun = Math.max(24, Math.round(height * 0.06))
|
|
172
|
+
|
|
173
|
+
for (let x = 0; x < width; x += 1) {
|
|
174
|
+
const runs = SchematicPowerDiagramLineMasks.#collectVerticalRuns(
|
|
175
|
+
darkMask,
|
|
176
|
+
gapBridgeMask,
|
|
177
|
+
width,
|
|
178
|
+
height,
|
|
179
|
+
x
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
183
|
+
if (!runs[index].connected) continue
|
|
184
|
+
|
|
185
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendColumnGapRun(
|
|
186
|
+
connectedMask,
|
|
187
|
+
gapBridgeMask,
|
|
188
|
+
queue,
|
|
189
|
+
width,
|
|
190
|
+
x,
|
|
191
|
+
runs[index],
|
|
192
|
+
runs[index + 1],
|
|
193
|
+
maximumGap,
|
|
194
|
+
minimumRun,
|
|
195
|
+
maximumRun,
|
|
196
|
+
writeIndex
|
|
197
|
+
)
|
|
198
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendColumnGapRun(
|
|
199
|
+
connectedMask,
|
|
200
|
+
gapBridgeMask,
|
|
201
|
+
queue,
|
|
202
|
+
width,
|
|
203
|
+
x,
|
|
204
|
+
runs[index],
|
|
205
|
+
runs[index - 1],
|
|
206
|
+
maximumGap,
|
|
207
|
+
minimumRun,
|
|
208
|
+
maximumRun,
|
|
209
|
+
writeIndex
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return writeIndex
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collects dark runs on one row.
|
|
219
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
220
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
221
|
+
* @param {number} width Image width.
|
|
222
|
+
* @param {number} y Row.
|
|
223
|
+
* @returns {{ start: number, end: number, connected: boolean }[]}
|
|
224
|
+
*/
|
|
225
|
+
static #collectHorizontalRuns(darkMask, connectedMask, width, y) {
|
|
226
|
+
const runs = []
|
|
227
|
+
let x = 0
|
|
228
|
+
|
|
229
|
+
while (x < width) {
|
|
230
|
+
while (x < width && !darkMask[y * width + x]) x += 1
|
|
231
|
+
if (x >= width) break
|
|
232
|
+
|
|
233
|
+
const start = x
|
|
234
|
+
let connected = false
|
|
235
|
+
|
|
236
|
+
while (x < width && darkMask[y * width + x]) {
|
|
237
|
+
if (connectedMask[y * width + x]) connected = true
|
|
238
|
+
x += 1
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
runs.push({ start, end: x - 1, connected })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return runs
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Collects dark runs on one column.
|
|
249
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
250
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
251
|
+
* @param {number} width Image width.
|
|
252
|
+
* @param {number} height Image height.
|
|
253
|
+
* @param {number} x Column.
|
|
254
|
+
* @returns {{ start: number, end: number, connected: boolean }[]}
|
|
255
|
+
*/
|
|
256
|
+
static #collectVerticalRuns(darkMask, connectedMask, width, height, x) {
|
|
257
|
+
const runs = []
|
|
258
|
+
let y = 0
|
|
259
|
+
|
|
260
|
+
while (y < height) {
|
|
261
|
+
while (y < height && !darkMask[y * width + x]) y += 1
|
|
262
|
+
if (y >= height) break
|
|
263
|
+
|
|
264
|
+
const start = y
|
|
265
|
+
let connected = false
|
|
266
|
+
|
|
267
|
+
while (y < height && darkMask[y * width + x]) {
|
|
268
|
+
if (connectedMask[y * width + x]) connected = true
|
|
269
|
+
y += 1
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
runs.push({ start, end: y - 1, connected })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return runs
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Appends one target run when it is close enough to a connected source run.
|
|
280
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
281
|
+
* @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
|
|
282
|
+
* @param {Uint32Array} queue Shared queue.
|
|
283
|
+
* @param {number} width Image width.
|
|
284
|
+
* @param {number} y Row.
|
|
285
|
+
* @param {{ start: number, end: number }} source Source run.
|
|
286
|
+
* @param {{ start: number, end: number, connected: boolean } | undefined} target Target run.
|
|
287
|
+
* @param {number} maximumGap Maximum bridge gap.
|
|
288
|
+
* @param {number} minimumRun Minimum target run length.
|
|
289
|
+
* @param {number} maximumRun Maximum target run length.
|
|
290
|
+
* @param {number} writeIndex Queue write index.
|
|
291
|
+
* @returns {number}
|
|
292
|
+
*/
|
|
293
|
+
static #appendGapRun(
|
|
294
|
+
connectedMask,
|
|
295
|
+
gapBridgeMask,
|
|
296
|
+
queue,
|
|
297
|
+
width,
|
|
298
|
+
y,
|
|
299
|
+
source,
|
|
300
|
+
target,
|
|
301
|
+
maximumGap,
|
|
302
|
+
minimumRun,
|
|
303
|
+
maximumRun,
|
|
304
|
+
writeIndex
|
|
305
|
+
) {
|
|
306
|
+
if (!target || target.connected) return writeIndex
|
|
307
|
+
|
|
308
|
+
const gap =
|
|
309
|
+
target.start > source.end
|
|
310
|
+
? target.start - source.end - 1
|
|
311
|
+
: source.start - target.end - 1
|
|
312
|
+
const runLength = target.end - target.start + 1
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
gap > maximumGap ||
|
|
316
|
+
runLength < minimumRun ||
|
|
317
|
+
runLength > maximumRun
|
|
318
|
+
) {
|
|
319
|
+
return writeIndex
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (let x = target.start; x <= target.end; x += 1) {
|
|
323
|
+
const targetIndex = y * width + x
|
|
324
|
+
if (connectedMask[targetIndex]) continue
|
|
325
|
+
|
|
326
|
+
connectedMask[targetIndex] = 1
|
|
327
|
+
gapBridgeMask[targetIndex] = 1
|
|
328
|
+
queue[writeIndex] = targetIndex
|
|
329
|
+
writeIndex += 1
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return writeIndex
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Appends one vertical target run when it is close enough to a connected
|
|
337
|
+
* source run.
|
|
338
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
339
|
+
* @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
|
|
340
|
+
* @param {Uint32Array} queue Shared queue.
|
|
341
|
+
* @param {number} width Image width.
|
|
342
|
+
* @param {number} x Column.
|
|
343
|
+
* @param {{ start: number, end: number }} source Source run.
|
|
344
|
+
* @param {{ start: number, end: number, connected: boolean } | undefined} target Target run.
|
|
345
|
+
* @param {number} maximumGap Maximum bridge gap.
|
|
346
|
+
* @param {number} minimumRun Minimum target run length.
|
|
347
|
+
* @param {number} maximumRun Maximum target run length.
|
|
348
|
+
* @param {number} writeIndex Queue write index.
|
|
349
|
+
* @returns {number}
|
|
350
|
+
*/
|
|
351
|
+
static #appendColumnGapRun(
|
|
352
|
+
connectedMask,
|
|
353
|
+
gapBridgeMask,
|
|
354
|
+
queue,
|
|
355
|
+
width,
|
|
356
|
+
x,
|
|
357
|
+
source,
|
|
358
|
+
target,
|
|
359
|
+
maximumGap,
|
|
360
|
+
minimumRun,
|
|
361
|
+
maximumRun,
|
|
362
|
+
writeIndex
|
|
363
|
+
) {
|
|
364
|
+
if (!target || target.connected) return writeIndex
|
|
365
|
+
|
|
366
|
+
const gap =
|
|
367
|
+
target.start > source.end
|
|
368
|
+
? target.start - source.end - 1
|
|
369
|
+
: source.start - target.end - 1
|
|
370
|
+
const runLength = target.end - target.start + 1
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
gap > maximumGap ||
|
|
374
|
+
runLength < minimumRun ||
|
|
375
|
+
runLength > maximumRun
|
|
376
|
+
) {
|
|
377
|
+
return writeIndex
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (let y = target.start; y <= target.end; y += 1) {
|
|
381
|
+
const targetIndex = y * width + x
|
|
382
|
+
if (connectedMask[targetIndex]) continue
|
|
383
|
+
|
|
384
|
+
connectedMask[targetIndex] = 1
|
|
385
|
+
gapBridgeMask[targetIndex] = 1
|
|
386
|
+
queue[writeIndex] = targetIndex
|
|
387
|
+
writeIndex += 1
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return writeIndex
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Removes tall, narrow orange components that are more likely to be
|
|
395
|
+
* ordinary wire fragments than power-port labels or caps.
|
|
396
|
+
* @param {Uint8Array} orangeMask Orange candidate pixels.
|
|
397
|
+
* @param {number} width Image width.
|
|
398
|
+
* @param {number} height Image height.
|
|
399
|
+
*/
|
|
400
|
+
static removeNarrowLineComponents(orangeMask, width, height) {
|
|
401
|
+
const seen = new Uint8Array(orangeMask.length)
|
|
402
|
+
const queue = new Uint32Array(orangeMask.length)
|
|
403
|
+
const maximumNarrowWidth = Math.max(8, Math.round(width * 0.0024))
|
|
404
|
+
const minimumTallHeight = Math.max(20, Math.round(height * 0.02))
|
|
405
|
+
|
|
406
|
+
for (let index = 0; index < orangeMask.length; index += 1) {
|
|
407
|
+
if (!orangeMask[index] || seen[index]) continue
|
|
408
|
+
|
|
409
|
+
const component = SchematicPowerDiagramLineMasks.#readComponent(
|
|
410
|
+
orangeMask,
|
|
411
|
+
seen,
|
|
412
|
+
queue,
|
|
413
|
+
width,
|
|
414
|
+
height,
|
|
415
|
+
index
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (
|
|
419
|
+
component.width <= maximumNarrowWidth &&
|
|
420
|
+
component.height >= minimumTallHeight &&
|
|
421
|
+
!SchematicPowerDiagramLineMasks.#hasDenseOrangeContext(
|
|
422
|
+
orangeMask,
|
|
423
|
+
width,
|
|
424
|
+
height,
|
|
425
|
+
component
|
|
426
|
+
)
|
|
427
|
+
) {
|
|
428
|
+
for (let cursor = 0; cursor < component.length; cursor += 1) {
|
|
429
|
+
orangeMask[queue[cursor]] = 0
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Appends all eight-connected dark neighbors.
|
|
437
|
+
* @param {Uint8Array} darkMask Dark source pixels.
|
|
438
|
+
* @param {Uint8Array} connectedMask Connected pixels.
|
|
439
|
+
* @param {Uint32Array} queue Shared queue.
|
|
440
|
+
* @param {number} width Image width.
|
|
441
|
+
* @param {number} height Image height.
|
|
442
|
+
* @param {number} x Pixel x.
|
|
443
|
+
* @param {number} y Pixel y.
|
|
444
|
+
* @param {number} writeIndex Queue write index.
|
|
445
|
+
* @returns {number}
|
|
446
|
+
*/
|
|
447
|
+
static #appendConnectedNeighbors(
|
|
448
|
+
darkMask,
|
|
449
|
+
connectedMask,
|
|
450
|
+
queue,
|
|
451
|
+
width,
|
|
452
|
+
height,
|
|
453
|
+
x,
|
|
454
|
+
y,
|
|
455
|
+
writeIndex
|
|
456
|
+
) {
|
|
457
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
458
|
+
const nextY = y + dy
|
|
459
|
+
if (nextY < 0 || nextY >= height) continue
|
|
460
|
+
|
|
461
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
462
|
+
if (dx === 0 && dy === 0) continue
|
|
463
|
+
|
|
464
|
+
const nextX = x + dx
|
|
465
|
+
if (nextX < 0 || nextX >= width) continue
|
|
466
|
+
|
|
467
|
+
const nextIndex = nextY * width + nextX
|
|
468
|
+
if (!darkMask[nextIndex] || connectedMask[nextIndex]) {
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
connectedMask[nextIndex] = 1
|
|
473
|
+
queue[writeIndex] = nextIndex
|
|
474
|
+
writeIndex += 1
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return writeIndex
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Reads one four-connected mask component into the shared queue.
|
|
483
|
+
* @param {Uint8Array} mask Source mask.
|
|
484
|
+
* @param {Uint8Array} seen Visited pixels.
|
|
485
|
+
* @param {Uint32Array} queue Shared queue.
|
|
486
|
+
* @param {number} width Image width.
|
|
487
|
+
* @param {number} height Image height.
|
|
488
|
+
* @param {number} startIndex Component seed.
|
|
489
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number, length: number }}
|
|
490
|
+
*/
|
|
491
|
+
static #readComponent(mask, seen, queue, width, height, startIndex) {
|
|
492
|
+
let readIndex = 0
|
|
493
|
+
let writeIndex = 0
|
|
494
|
+
let minX = width
|
|
495
|
+
let minY = height
|
|
496
|
+
let maxX = 0
|
|
497
|
+
let maxY = 0
|
|
498
|
+
|
|
499
|
+
seen[startIndex] = 1
|
|
500
|
+
queue[writeIndex] = startIndex
|
|
501
|
+
writeIndex += 1
|
|
502
|
+
|
|
503
|
+
while (readIndex < writeIndex) {
|
|
504
|
+
const index = queue[readIndex]
|
|
505
|
+
readIndex += 1
|
|
506
|
+
|
|
507
|
+
const x = index % width
|
|
508
|
+
const y = Math.floor(index / width)
|
|
509
|
+
minX = Math.min(minX, x)
|
|
510
|
+
minY = Math.min(minY, y)
|
|
511
|
+
maxX = Math.max(maxX, x)
|
|
512
|
+
maxY = Math.max(maxY, y)
|
|
513
|
+
|
|
514
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
|
|
515
|
+
mask,
|
|
516
|
+
seen,
|
|
517
|
+
queue,
|
|
518
|
+
width,
|
|
519
|
+
height,
|
|
520
|
+
x - 1,
|
|
521
|
+
y,
|
|
522
|
+
writeIndex
|
|
523
|
+
)
|
|
524
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
|
|
525
|
+
mask,
|
|
526
|
+
seen,
|
|
527
|
+
queue,
|
|
528
|
+
width,
|
|
529
|
+
height,
|
|
530
|
+
x + 1,
|
|
531
|
+
y,
|
|
532
|
+
writeIndex
|
|
533
|
+
)
|
|
534
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
|
|
535
|
+
mask,
|
|
536
|
+
seen,
|
|
537
|
+
queue,
|
|
538
|
+
width,
|
|
539
|
+
height,
|
|
540
|
+
x,
|
|
541
|
+
y - 1,
|
|
542
|
+
writeIndex
|
|
543
|
+
)
|
|
544
|
+
writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
|
|
545
|
+
mask,
|
|
546
|
+
seen,
|
|
547
|
+
queue,
|
|
548
|
+
width,
|
|
549
|
+
height,
|
|
550
|
+
x,
|
|
551
|
+
y + 1,
|
|
552
|
+
writeIndex
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
minX,
|
|
558
|
+
minY,
|
|
559
|
+
maxX,
|
|
560
|
+
maxY,
|
|
561
|
+
width: maxX - minX + 1,
|
|
562
|
+
height: maxY - minY + 1,
|
|
563
|
+
length: writeIndex
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Appends one component neighbor when it is in-bounds and unvisited.
|
|
569
|
+
* @param {Uint8Array} mask Source mask.
|
|
570
|
+
* @param {Uint8Array} seen Visited pixels.
|
|
571
|
+
* @param {Uint32Array} queue Shared queue.
|
|
572
|
+
* @param {number} width Image width.
|
|
573
|
+
* @param {number} height Image height.
|
|
574
|
+
* @param {number} x Pixel x.
|
|
575
|
+
* @param {number} y Pixel y.
|
|
576
|
+
* @param {number} writeIndex Queue write index.
|
|
577
|
+
* @returns {number}
|
|
578
|
+
*/
|
|
579
|
+
static #appendComponentPixel(
|
|
580
|
+
mask,
|
|
581
|
+
seen,
|
|
582
|
+
queue,
|
|
583
|
+
width,
|
|
584
|
+
height,
|
|
585
|
+
x,
|
|
586
|
+
y,
|
|
587
|
+
writeIndex
|
|
588
|
+
) {
|
|
589
|
+
if (x < 0 || x >= width || y < 0 || y >= height) return writeIndex
|
|
590
|
+
|
|
591
|
+
const index = y * width + x
|
|
592
|
+
if (!mask[index] || seen[index]) return writeIndex
|
|
593
|
+
|
|
594
|
+
seen[index] = 1
|
|
595
|
+
queue[writeIndex] = index
|
|
596
|
+
|
|
597
|
+
return writeIndex + 1
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Returns true when a narrow component sits inside a dense orange word or
|
|
602
|
+
* cap context.
|
|
603
|
+
* @param {Uint8Array} orangeMask Orange candidate pixels.
|
|
604
|
+
* @param {number} width Image width.
|
|
605
|
+
* @param {number} height Image height.
|
|
606
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} component Component bounds.
|
|
607
|
+
* @returns {boolean}
|
|
608
|
+
*/
|
|
609
|
+
static #hasDenseOrangeContext(orangeMask, width, height, component) {
|
|
610
|
+
const contextX = Math.max(24, Math.round(width * 0.045))
|
|
611
|
+
const contextY = Math.max(8, Math.round(height * 0.015))
|
|
612
|
+
const minimumDenseRowPixels = Math.max(16, Math.round(width * 0.003))
|
|
613
|
+
const left = Math.max(0, component.minX - contextX)
|
|
614
|
+
const right = Math.min(width - 1, component.maxX + contextX)
|
|
615
|
+
const top = Math.max(0, component.minY - contextY)
|
|
616
|
+
const bottom = Math.min(height - 1, component.maxY + contextY)
|
|
617
|
+
|
|
618
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
619
|
+
let rowPixels = 0
|
|
620
|
+
const rowOffset = y * width
|
|
621
|
+
|
|
622
|
+
for (let x = left; x <= right; x += 1) {
|
|
623
|
+
if (orangeMask[rowOffset + x]) rowPixels += 1
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (rowPixels >= minimumDenseRowPixels) return true
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return false
|
|
630
|
+
}
|
|
631
|
+
}
|
|
@@ -36,7 +36,7 @@ export class SchematicPowerPortRenderer {
|
|
|
36
36
|
fontStyle: text.fontStyle
|
|
37
37
|
})
|
|
38
38
|
const fontSize = Number(labelOptions.fontSize || 9)
|
|
39
|
-
const resolvedColor = SchematicColorResolver.
|
|
39
|
+
const resolvedColor = SchematicColorResolver.resolveNonTextColor(
|
|
40
40
|
text.color,
|
|
41
41
|
'--schematic-power-color'
|
|
42
42
|
)
|