altium-toolkit 1.0.1 → 1.0.7
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/AltiumParser.mjs +17 -6
- package/src/core/altium/AsciiRecordParser.mjs +28 -1
- package/src/core/altium/SchematicPinDesignatorInferer.mjs +36 -3
- package/src/core/altium/SchematicPinParser.mjs +139 -14
- package/src/core/altium/SchematicPrimitiveParser.mjs +75 -4
- package/src/core/altium/SchematicTextParser.mjs +22 -18
- package/src/core/altium/SchematicTextPostProcessor.mjs +127 -10
- package/src/core/altium/SchematicWireNormalizer.mjs +1162 -0
- package/src/styles/altium-renderers.css +12 -1
- package/src/ui/PcbNativeTextKnockoutDetector.mjs +130 -0
- package/src/ui/PcbSvgRenderer.mjs +83 -10
- package/src/ui/PcbTextPrimitiveRenderer.mjs +704 -19
- package/src/ui/SchematicContentLayout.mjs +124 -22
- package/src/ui/SchematicJunctionRenderer.mjs +21 -3
- package/src/ui/SchematicNoteRenderer.mjs +75 -10
- package/src/ui/SchematicPinSvgRenderer.mjs +48 -7
- package/src/ui/SchematicPowerPortRenderer.mjs +53 -160
- package/src/ui/SchematicSheetChromeRenderer.mjs +29 -15
- package/src/ui/SchematicSvgRenderer.mjs +341 -39
- package/src/ui/SchematicSvgUtils.mjs +9 -2
- package/src/ui/SchematicTypography.mjs +13 -10
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
--pcb-via-hole-fill: #0f746c;
|
|
142
142
|
--pcb-footprint-fill: rgba(247, 230, 117, 0.14);
|
|
143
143
|
--pcb-footprint-track-color: rgba(237, 172, 36, 1);
|
|
144
|
+
--pcb-text-knockout-fill: rgba(248, 246, 239, 0.96);
|
|
144
145
|
--pcb-component-top-fill: rgba(244, 219, 198, 0.92);
|
|
145
146
|
--pcb-component-bottom-fill: rgba(15, 116, 108, 0.84);
|
|
146
147
|
--pcb-component-stroke: rgba(110, 64, 38, 0.28);
|
|
@@ -178,6 +179,8 @@
|
|
|
178
179
|
fill: var(--pcb-board-fill);
|
|
179
180
|
stroke: var(--pcb-board-stroke);
|
|
180
181
|
stroke-width: 18;
|
|
182
|
+
stroke-linecap: round;
|
|
183
|
+
stroke-linejoin: round;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
.board-outline--stroke {
|
|
@@ -252,7 +255,7 @@
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
.pcb-footprint-region {
|
|
255
|
-
fill: var(--pcb-footprint-track-color);
|
|
258
|
+
fill: var(--pcb-footprint-region-fill, var(--pcb-footprint-track-color));
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
.pcb-footprint-track,
|
|
@@ -278,6 +281,14 @@
|
|
|
278
281
|
pointer-events: none;
|
|
279
282
|
}
|
|
280
283
|
|
|
284
|
+
.pcb-text__knockout-fill {
|
|
285
|
+
fill: var(--pcb-text-knockout-fill);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.pcb-text__knockout-glyphs {
|
|
289
|
+
fill: #000;
|
|
290
|
+
}
|
|
291
|
+
|
|
281
292
|
.bom-panel {
|
|
282
293
|
display: grid;
|
|
283
294
|
gap: 1rem;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects native overlay artwork that already contains text knockout holes.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbNativeTextKnockoutDetector {
|
|
9
|
+
static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
|
|
10
|
+
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true when side-resolved overlay primitives carry native text
|
|
14
|
+
* knockouts, so source inverted TrueType labels would duplicate artwork.
|
|
15
|
+
* @param {{ fills?: object[], regions?: object[], tracks?: object[], arcs?: object[] }} primitives Side-resolved overlay primitives.
|
|
16
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
static hasNativeTextKnockouts(primitives, board) {
|
|
20
|
+
const fills = [
|
|
21
|
+
...(Array.isArray(primitives?.fills) ? primitives.fills : []),
|
|
22
|
+
...(Array.isArray(primitives?.regions) ? primitives.regions : [])
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
PcbNativeTextKnockoutDetector.#isDenseOverlayArtwork(
|
|
27
|
+
{
|
|
28
|
+
fills,
|
|
29
|
+
tracks: primitives?.tracks,
|
|
30
|
+
arcs: primitives?.arcs
|
|
31
|
+
},
|
|
32
|
+
board
|
|
33
|
+
) &&
|
|
34
|
+
fills.some(
|
|
35
|
+
(fill) => Array.isArray(fill?.holes) && fill.holes.length > 0
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detects dense overlay artwork from structural density.
|
|
42
|
+
* @param {{ fills?: object[], tracks?: object[], arcs?: object[] }} side Side primitives.
|
|
43
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
static #isDenseOverlayArtwork(side, board) {
|
|
47
|
+
const strokeCount =
|
|
48
|
+
(Array.isArray(side?.tracks) ? side.tracks.length : 0) +
|
|
49
|
+
(Array.isArray(side?.arcs) ? side.arcs.length : 0)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
strokeCount >=
|
|
53
|
+
PcbNativeTextKnockoutDetector.#DENSE_OVERLAY_MIN_TRACK_COUNT &&
|
|
54
|
+
PcbNativeTextKnockoutDetector.#maxFillAreaRatio(
|
|
55
|
+
side?.fills,
|
|
56
|
+
board
|
|
57
|
+
) >=
|
|
58
|
+
PcbNativeTextKnockoutDetector
|
|
59
|
+
.#DENSE_OVERLAY_MIN_REGION_AREA_RATIO
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the largest fill-to-board bounding-box area ratio.
|
|
65
|
+
* @param {object[] | undefined} fills Fill primitives.
|
|
66
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
static #maxFillAreaRatio(fills, board) {
|
|
70
|
+
const boardArea =
|
|
71
|
+
Math.max(Number(board?.widthMil || 0), 0) *
|
|
72
|
+
Math.max(Number(board?.heightMil || 0), 0)
|
|
73
|
+
if (!boardArea) {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (Array.isArray(fills) ? fills : []).reduce((maxRatio, fill) => {
|
|
78
|
+
const bounds =
|
|
79
|
+
PcbNativeTextKnockoutDetector.#resolveFillBounds(fill)
|
|
80
|
+
if (!bounds) {
|
|
81
|
+
return maxRatio
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fillArea =
|
|
85
|
+
Math.max(bounds.maxX - bounds.minX, 0) *
|
|
86
|
+
Math.max(bounds.maxY - bounds.minY, 0)
|
|
87
|
+
|
|
88
|
+
return Math.max(maxRatio, fillArea / boardArea)
|
|
89
|
+
}, 0)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves rough authored bounds for one rectangular or polygon fill.
|
|
94
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, points?: { x?: number, y?: number }[] }} fill Fill primitive.
|
|
95
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
96
|
+
*/
|
|
97
|
+
static #resolveFillBounds(fill) {
|
|
98
|
+
const points = Array.isArray(fill?.points)
|
|
99
|
+
? fill.points
|
|
100
|
+
.map((point) => ({
|
|
101
|
+
x: Number(point?.x),
|
|
102
|
+
y: Number(point?.y)
|
|
103
|
+
}))
|
|
104
|
+
.filter(
|
|
105
|
+
(point) =>
|
|
106
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
107
|
+
)
|
|
108
|
+
: [
|
|
109
|
+
{ x: Number(fill?.x1), y: Number(fill?.y1) },
|
|
110
|
+
{ x: Number(fill?.x2), y: Number(fill?.y2) }
|
|
111
|
+
].filter(
|
|
112
|
+
(point) =>
|
|
113
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (points.length < 2) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const xs = points.map((point) => point.x)
|
|
121
|
+
const ys = points.map((point) => point.y)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
minX: Math.min(...xs),
|
|
125
|
+
minY: Math.min(...ys),
|
|
126
|
+
maxX: Math.max(...xs),
|
|
127
|
+
maxY: Math.max(...ys)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -7,7 +7,9 @@ import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs
|
|
|
7
7
|
import { PcbEmbeddedFontFaceRenderer } from './PcbEmbeddedFontFaceRenderer.mjs'
|
|
8
8
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
9
9
|
import { PcbCopperPrimitiveSplitter } from './PcbCopperPrimitiveSplitter.mjs'
|
|
10
|
+
import { PcbNativeTextKnockoutDetector } from './PcbNativeTextKnockoutDetector.mjs'
|
|
10
11
|
import { PcbRegionPrimitiveRenderer } from './PcbRegionPrimitiveRenderer.mjs'
|
|
12
|
+
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
11
13
|
import { PcbTextPrimitiveRenderer } from './PcbTextPrimitiveRenderer.mjs'
|
|
12
14
|
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
13
15
|
/**
|
|
@@ -27,7 +29,10 @@ export class PcbSvgRenderer {
|
|
|
27
29
|
if (!pcb) {
|
|
28
30
|
return '<section class="altium-renderer-empty">No PCB entities were recovered from this file.</section>'
|
|
29
31
|
}
|
|
30
|
-
const outline =
|
|
32
|
+
const outline = PcbScene3dBoardOutlineRefiner.refine(
|
|
33
|
+
{ board: pcb.boardOutline },
|
|
34
|
+
documentModel
|
|
35
|
+
).board
|
|
31
36
|
const polygons = pcb.polygons || []
|
|
32
37
|
const fills = pcb.fills || []
|
|
33
38
|
const tracks = pcb.tracks || []
|
|
@@ -43,11 +48,6 @@ export class PcbSvgRenderer {
|
|
|
43
48
|
const stackLayers = Array.isArray(pcb.layers) ? pcb.layers : []
|
|
44
49
|
const primitiveLayers = pcb.primitiveLayers || []
|
|
45
50
|
const displayLayers = stackLayers.length ? stackLayers : primitiveLayers
|
|
46
|
-
const texts = PcbTextPrimitiveRenderer.select(
|
|
47
|
-
primitiveLayers,
|
|
48
|
-
pcb.texts || [],
|
|
49
|
-
'top'
|
|
50
|
-
)
|
|
51
51
|
const copperGroups = PcbCopperPrimitiveSplitter.split(
|
|
52
52
|
polygons,
|
|
53
53
|
fills,
|
|
@@ -66,6 +66,18 @@ export class PcbSvgRenderer {
|
|
|
66
66
|
),
|
|
67
67
|
outline
|
|
68
68
|
)
|
|
69
|
+
const texts = PcbTextPrimitiveRenderer.select(
|
|
70
|
+
primitiveLayers,
|
|
71
|
+
pcb.texts || [],
|
|
72
|
+
'top',
|
|
73
|
+
{
|
|
74
|
+
nativeTextKnockouts:
|
|
75
|
+
PcbNativeTextKnockoutDetector.hasNativeTextKnockouts(
|
|
76
|
+
footprintPrimitives,
|
|
77
|
+
outline
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
69
81
|
const path = PcbSvgRenderer.#buildBoardPath(outline.segments)
|
|
70
82
|
const clipPathId = 'pcb-board-clip'
|
|
71
83
|
const viewBox = PcbSvgRenderer.#buildViewBox(
|
|
@@ -256,6 +268,9 @@ export class PcbSvgRenderer {
|
|
|
256
268
|
'pcb-footprint-region'
|
|
257
269
|
)
|
|
258
270
|
const textMarkup = PcbTextPrimitiveRenderer.render(texts)
|
|
271
|
+
const textGroupTransform = PcbSvgRenderer.#renderTextGroupTransform(
|
|
272
|
+
pcb.textGroupTransform
|
|
273
|
+
)
|
|
259
274
|
const fontFaceMarkup = PcbEmbeddedFontFaceRenderer.buildMarkup(
|
|
260
275
|
pcb.embeddedFonts || []
|
|
261
276
|
)
|
|
@@ -355,17 +370,19 @@ export class PcbSvgRenderer {
|
|
|
355
370
|
footprintArcMarkup +
|
|
356
371
|
footprintRegionMarkup +
|
|
357
372
|
'</g>' +
|
|
373
|
+
'<g class="pcb-components">' +
|
|
374
|
+
componentMarkup +
|
|
375
|
+
'</g>' +
|
|
358
376
|
'<g class="pcb-texts" clip-path="url(#' +
|
|
359
377
|
clipPathId +
|
|
360
|
-
')"
|
|
378
|
+
')"' +
|
|
379
|
+
textGroupTransform +
|
|
380
|
+
'>' +
|
|
361
381
|
textMarkup +
|
|
362
382
|
'</g>' +
|
|
363
383
|
'<path class="board-outline board-outline--stroke" d="' +
|
|
364
384
|
SchematicSvgUtils.escapeHtml(path) +
|
|
365
385
|
'" />' +
|
|
366
|
-
'<g class="pcb-components">' +
|
|
367
|
-
componentMarkup +
|
|
368
|
-
'</g>' +
|
|
369
386
|
'</svg></div></section>'
|
|
370
387
|
)
|
|
371
388
|
}
|
|
@@ -415,6 +432,62 @@ export class PcbSvgRenderer {
|
|
|
415
432
|
return path + ' Z'
|
|
416
433
|
}
|
|
417
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Renders an optional transform for the whole PCB text layer.
|
|
437
|
+
* @param {{ translateX?: number, translateY?: number, scaleX?: number, scaleY?: number } | undefined} transform Text group transform.
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
static #renderTextGroupTransform(transform) {
|
|
441
|
+
if (!transform || typeof transform !== 'object') {
|
|
442
|
+
return ''
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const translateX = Number(transform.translateX || 0)
|
|
446
|
+
const translateY = Number(transform.translateY || 0)
|
|
447
|
+
const scaleX =
|
|
448
|
+
transform.scaleX === null || transform.scaleX === undefined
|
|
449
|
+
? 1
|
|
450
|
+
: Number(transform.scaleX)
|
|
451
|
+
const scaleY =
|
|
452
|
+
transform.scaleY === null || transform.scaleY === undefined
|
|
453
|
+
? 1
|
|
454
|
+
: Number(transform.scaleY)
|
|
455
|
+
if (
|
|
456
|
+
!Number.isFinite(translateX) ||
|
|
457
|
+
!Number.isFinite(translateY) ||
|
|
458
|
+
!Number.isFinite(scaleX) ||
|
|
459
|
+
!Number.isFinite(scaleY) ||
|
|
460
|
+
(translateX === 0 &&
|
|
461
|
+
translateY === 0 &&
|
|
462
|
+
scaleX === 1 &&
|
|
463
|
+
scaleY === 1)
|
|
464
|
+
) {
|
|
465
|
+
return ''
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const parts = []
|
|
469
|
+
if (translateX !== 0 || translateY !== 0) {
|
|
470
|
+
parts.push(
|
|
471
|
+
'translate(' +
|
|
472
|
+
SchematicSvgUtils.formatNumber(translateX) +
|
|
473
|
+
' ' +
|
|
474
|
+
SchematicSvgUtils.formatNumber(translateY) +
|
|
475
|
+
')'
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
479
|
+
parts.push(
|
|
480
|
+
'scale(' +
|
|
481
|
+
SchematicSvgUtils.formatNumber(scaleX) +
|
|
482
|
+
' ' +
|
|
483
|
+
SchematicSvgUtils.formatNumber(scaleY) +
|
|
484
|
+
')'
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return ' transform="' + parts.join(' ') + '"'
|
|
489
|
+
}
|
|
490
|
+
|
|
418
491
|
/**
|
|
419
492
|
* Computes a reasonable viewBox.
|
|
420
493
|
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }} outline
|