altium-toolkit 0.1.0
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +20 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +22 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/GPL-3.0-or-later.txt +232 -0
- package/NOTICE.md +32 -0
- package/README.md +116 -0
- package/docs/api.md +73 -0
- package/docs/model-format.md +36 -0
- package/docs/testing.md +25 -0
- package/examples/README.md +47 -0
- package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
- package/examples/arduino-uno/SvgViewportController.mjs +306 -0
- package/examples/arduino-uno/example.mjs +480 -0
- package/examples/arduino-uno/index.html +163 -0
- package/examples/arduino-uno/styles.css +552 -0
- package/examples/server.mjs +212 -0
- package/package.json +53 -0
- package/spec/library-scope.md +32 -0
- package/src/core/BinaryReader.mjs +127 -0
- package/src/core/altium/AltiumLayoutParser.mjs +485 -0
- package/src/core/altium/AltiumParser.mjs +1007 -0
- package/src/core/altium/AsciiRecordParser.mjs +151 -0
- package/src/core/altium/ParserUtils.mjs +173 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
- package/src/core/altium/PcbModelParser.mjs +336 -0
- package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
- package/src/core/altium/PcbStreamExtractor.mjs +210 -0
- package/src/core/altium/PrintableTextDecoder.mjs +156 -0
- package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
- package/src/core/altium/SchematicImageParser.mjs +173 -0
- package/src/core/altium/SchematicJunctionParser.mjs +43 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
- package/src/core/altium/SchematicPinParser.mjs +767 -0
- package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
- package/src/core/altium/SchematicSheetParser.mjs +241 -0
- package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
- package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
- package/src/core/altium/SchematicTextParser.mjs +708 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
- package/src/core/ole/OleCompoundDocument.mjs +439 -0
- package/src/core/ole/OleConstants.mjs +64 -0
- package/src/core/ole/OleDirectoryEntry.mjs +95 -0
- package/src/index.mjs +7 -0
- package/src/parser.mjs +21 -0
- package/src/renderers.mjs +15 -0
- package/src/scene3d.mjs +9 -0
- package/src/styles/altium-renderers.css +358 -0
- package/src/ui/BomTableRenderer.mjs +46 -0
- package/src/ui/PcbArcUtils.mjs +189 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
- package/src/ui/PcbScene3dBuilder.mjs +742 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
- package/src/ui/PcbScene3dPackages.mjs +137 -0
- package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
- package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
- package/src/ui/PcbSvgRenderer.mjs +906 -0
- package/src/ui/SchematicColorResolver.mjs +132 -0
- package/src/ui/SchematicContentLayout.mjs +661 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
- package/src/ui/SchematicImageRenderer.mjs +135 -0
- package/src/ui/SchematicJunctionRenderer.mjs +381 -0
- package/src/ui/SchematicNoteRenderer.mjs +427 -0
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
- package/src/ui/SchematicPortRenderer.mjs +558 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
- package/src/ui/SchematicRegionRenderer.mjs +94 -0
- package/src/ui/SchematicShapeRenderer.mjs +398 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
- package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
- package/src/ui/SchematicSvgRenderer.mjs +756 -0
- package/src/ui/SchematicSvgUtils.mjs +182 -0
- package/src/ui/SchematicTypography.mjs +204 -0
- package/src/workers/altium-parser.worker.mjs +29 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
7
|
+
import { SchematicTypography } from './SchematicTypography.mjs'
|
|
8
|
+
|
|
9
|
+
const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
|
|
10
|
+
SchematicSvgUtils
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders normalized schematic directives into SVG markup.
|
|
14
|
+
*/
|
|
15
|
+
export class SchematicDirectiveRenderer {
|
|
16
|
+
/**
|
|
17
|
+
* Builds directive markup for supported schematic directive primitives.
|
|
18
|
+
* @param {{ x: number, y: number, color: string, name: string, orientation?: number }[]} directives
|
|
19
|
+
* @param {number} sheetHeight
|
|
20
|
+
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static buildMarkup(directives, sheetHeight, sheet) {
|
|
24
|
+
return directives
|
|
25
|
+
.map((directive) =>
|
|
26
|
+
SchematicDirectiveRenderer.#buildDirectiveMarkup(
|
|
27
|
+
directive,
|
|
28
|
+
sheetHeight,
|
|
29
|
+
sheet
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
.join('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds one supported directive glyph.
|
|
37
|
+
* @param {{ x: number, y: number, color: string, name: string, orientation?: number }} directive
|
|
38
|
+
* @param {number} sheetHeight
|
|
39
|
+
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
static #buildDirectiveMarkup(directive, sheetHeight, sheet) {
|
|
43
|
+
switch (String(directive?.name || '').toUpperCase()) {
|
|
44
|
+
case 'DIFFPAIR':
|
|
45
|
+
return SchematicDirectiveRenderer.#buildDiffPairMarkup(
|
|
46
|
+
directive,
|
|
47
|
+
sheetHeight
|
|
48
|
+
)
|
|
49
|
+
case 'DIFFPAIRROUTING':
|
|
50
|
+
return SchematicDirectiveRenderer.#buildRouteMarkup(
|
|
51
|
+
directive,
|
|
52
|
+
sheetHeight,
|
|
53
|
+
sheet
|
|
54
|
+
)
|
|
55
|
+
default:
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Builds the labeled route-callout marker for one differential-pair
|
|
62
|
+
* routing directive.
|
|
63
|
+
* @param {{ x: number, y: number, color: string, orientation?: number }} directive
|
|
64
|
+
* @param {number} sheetHeight
|
|
65
|
+
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
static #buildRouteMarkup(directive, sheetHeight, sheet) {
|
|
69
|
+
const color = SchematicColorResolver.resolveColor(
|
|
70
|
+
directive.color,
|
|
71
|
+
'--schematic-alert-color'
|
|
72
|
+
)
|
|
73
|
+
const projectedY = projectSchematicY(sheetHeight, directive.y)
|
|
74
|
+
const verticalDirection =
|
|
75
|
+
Number(directive.orientation || 0) === 3 ? 1 : -1
|
|
76
|
+
const circleRadius = 7
|
|
77
|
+
const circleCenterY = projectedY + verticalDirection * 18
|
|
78
|
+
const leaderEndY = circleCenterY - verticalDirection * circleRadius
|
|
79
|
+
const labelOptions =
|
|
80
|
+
SchematicTypography.buildViewerSchematicFontOptions(sheet)
|
|
81
|
+
const infoOptions = {
|
|
82
|
+
...labelOptions,
|
|
83
|
+
fontSize: Math.max(Number(labelOptions.fontSize || 9) - 1, 6),
|
|
84
|
+
fontWeight: 700
|
|
85
|
+
}
|
|
86
|
+
const labelY =
|
|
87
|
+
circleCenterY +
|
|
88
|
+
verticalDirection *
|
|
89
|
+
(circleRadius + Number(labelOptions.fontSize || 9))
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
'<g class="schematic-directive schematic-directive--route">' +
|
|
93
|
+
'<line x1="' +
|
|
94
|
+
formatNumber(directive.x) +
|
|
95
|
+
'" y1="' +
|
|
96
|
+
formatNumber(projectedY) +
|
|
97
|
+
'" x2="' +
|
|
98
|
+
formatNumber(directive.x) +
|
|
99
|
+
'" y2="' +
|
|
100
|
+
formatNumber(leaderEndY) +
|
|
101
|
+
'" stroke="' +
|
|
102
|
+
escapeHtml(color) +
|
|
103
|
+
'" stroke-width="1" />' +
|
|
104
|
+
'<circle cx="' +
|
|
105
|
+
formatNumber(directive.x) +
|
|
106
|
+
'" cy="' +
|
|
107
|
+
formatNumber(circleCenterY) +
|
|
108
|
+
'" r="' +
|
|
109
|
+
formatNumber(circleRadius) +
|
|
110
|
+
'" fill="none" stroke="' +
|
|
111
|
+
escapeHtml(color) +
|
|
112
|
+
'" stroke-width="1" />' +
|
|
113
|
+
createSvgText(
|
|
114
|
+
'schematic-directive-label',
|
|
115
|
+
directive.x,
|
|
116
|
+
labelY,
|
|
117
|
+
String(directive.name || ''),
|
|
118
|
+
color,
|
|
119
|
+
'middle',
|
|
120
|
+
labelOptions
|
|
121
|
+
) +
|
|
122
|
+
createSvgText(
|
|
123
|
+
'schematic-directive-info',
|
|
124
|
+
directive.x,
|
|
125
|
+
circleCenterY + Number(infoOptions.fontSize || 8) * 0.34,
|
|
126
|
+
'i',
|
|
127
|
+
color,
|
|
128
|
+
'middle',
|
|
129
|
+
infoOptions
|
|
130
|
+
) +
|
|
131
|
+
'</g>'
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Builds the paired-trace differential-pair marker glyph.
|
|
137
|
+
* @param {{ x: number, y: number, color: string }} directive
|
|
138
|
+
* @param {number} sheetHeight
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
static #buildDiffPairMarkup(directive, sheetHeight) {
|
|
142
|
+
const color = SchematicColorResolver.resolveColor(
|
|
143
|
+
directive.color,
|
|
144
|
+
'--schematic-alert-color'
|
|
145
|
+
)
|
|
146
|
+
const centerY = projectSchematicY(sheetHeight, directive.y)
|
|
147
|
+
const topPoints = [
|
|
148
|
+
[directive.x - 10, centerY - 2],
|
|
149
|
+
[directive.x - 4, centerY - 2],
|
|
150
|
+
[directive.x, centerY - 6],
|
|
151
|
+
[directive.x + 4, centerY - 6],
|
|
152
|
+
[directive.x + 8, centerY - 2],
|
|
153
|
+
[directive.x + 14, centerY - 2]
|
|
154
|
+
]
|
|
155
|
+
const bottomPoints = topPoints.map(([x, y]) => [x, y + 8])
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
'<g class="schematic-directive schematic-directive--pair">' +
|
|
159
|
+
SchematicDirectiveRenderer.#buildPolyline(topPoints, color) +
|
|
160
|
+
SchematicDirectiveRenderer.#buildPolyline(bottomPoints, color) +
|
|
161
|
+
'</g>'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Builds one open SVG polyline for a directive glyph.
|
|
167
|
+
* @param {number[][]} points
|
|
168
|
+
* @param {string} color
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
static #buildPolyline(points, color) {
|
|
172
|
+
return (
|
|
173
|
+
'<polyline points="' +
|
|
174
|
+
escapeHtml(
|
|
175
|
+
points
|
|
176
|
+
.map(([x, y]) => formatNumber(x) + ',' + formatNumber(y))
|
|
177
|
+
.join(' ')
|
|
178
|
+
) +
|
|
179
|
+
'" fill="none" stroke="' +
|
|
180
|
+
escapeHtml(color) +
|
|
181
|
+
'" stroke-width="1" stroke-linejoin="round" stroke-linecap="round" />'
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
7
|
+
|
|
8
|
+
const { escapeHtml, formatNumber, projectSchematicY } = SchematicSvgUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders normalized schematic image placements.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicImageRenderer {
|
|
14
|
+
/**
|
|
15
|
+
* Builds markup for embedded schematic images and unresolved placeholders.
|
|
16
|
+
* @param {{ x: number, y: number, cornerX: number, cornerY: number, mimeType?: string, dataBase64?: string, diagnosticState?: string, keepAspect?: boolean }[]} images
|
|
17
|
+
* @param {number} sheetHeight
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
static buildMarkup(images, sheetHeight) {
|
|
21
|
+
return images
|
|
22
|
+
.map((image) =>
|
|
23
|
+
image.dataBase64 && image.mimeType
|
|
24
|
+
? SchematicImageRenderer.#buildEmbeddedImageMarkup(
|
|
25
|
+
image,
|
|
26
|
+
sheetHeight
|
|
27
|
+
)
|
|
28
|
+
: SchematicImageRenderer.#buildPlaceholderMarkup(
|
|
29
|
+
image,
|
|
30
|
+
sheetHeight
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
.join('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Builds one embedded SVG image node.
|
|
38
|
+
* @param {{ x: number, y: number, cornerX: number, cornerY: number, mimeType: string, dataBase64: string, keepAspect?: boolean }} image
|
|
39
|
+
* @param {number} sheetHeight
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
static #buildEmbeddedImageMarkup(image, sheetHeight) {
|
|
43
|
+
const bounds = SchematicImageRenderer.#resolveBounds(image, sheetHeight)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
'<image class="schematic-embedded-image" x="' +
|
|
47
|
+
formatNumber(bounds.x) +
|
|
48
|
+
'" y="' +
|
|
49
|
+
formatNumber(bounds.y) +
|
|
50
|
+
'" width="' +
|
|
51
|
+
formatNumber(bounds.width) +
|
|
52
|
+
'" height="' +
|
|
53
|
+
formatNumber(bounds.height) +
|
|
54
|
+
'" preserveAspectRatio="' +
|
|
55
|
+
escapeHtml(image.keepAspect === false ? 'none' : 'xMidYMid meet') +
|
|
56
|
+
'" href="' +
|
|
57
|
+
escapeHtml(
|
|
58
|
+
'data:' + image.mimeType + ';base64,' + image.dataBase64
|
|
59
|
+
) +
|
|
60
|
+
'" />'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds one placeholder frame when an image payload is unavailable.
|
|
66
|
+
* @param {{ x: number, y: number, cornerX: number, cornerY: number }} image
|
|
67
|
+
* @param {number} sheetHeight
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
static #buildPlaceholderMarkup(image, sheetHeight) {
|
|
71
|
+
const bounds = SchematicImageRenderer.#resolveBounds(image, sheetHeight)
|
|
72
|
+
const stroke = SchematicColorResolver.resolveColor(
|
|
73
|
+
'#c0c0c0',
|
|
74
|
+
'--schematic-note-border-color'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
'<g class="schematic-image-placeholder">' +
|
|
79
|
+
'<rect x="' +
|
|
80
|
+
formatNumber(bounds.x) +
|
|
81
|
+
'" y="' +
|
|
82
|
+
formatNumber(bounds.y) +
|
|
83
|
+
'" width="' +
|
|
84
|
+
formatNumber(bounds.width) +
|
|
85
|
+
'" height="' +
|
|
86
|
+
formatNumber(bounds.height) +
|
|
87
|
+
'" fill="none" stroke="' +
|
|
88
|
+
escapeHtml(stroke) +
|
|
89
|
+
'" stroke-width="1" />' +
|
|
90
|
+
'<line x1="' +
|
|
91
|
+
formatNumber(bounds.x) +
|
|
92
|
+
'" y1="' +
|
|
93
|
+
formatNumber(bounds.y) +
|
|
94
|
+
'" x2="' +
|
|
95
|
+
formatNumber(bounds.x + bounds.width) +
|
|
96
|
+
'" y2="' +
|
|
97
|
+
formatNumber(bounds.y + bounds.height) +
|
|
98
|
+
'" stroke="' +
|
|
99
|
+
escapeHtml(stroke) +
|
|
100
|
+
'" stroke-width="1" />' +
|
|
101
|
+
'<line x1="' +
|
|
102
|
+
formatNumber(bounds.x) +
|
|
103
|
+
'" y1="' +
|
|
104
|
+
formatNumber(bounds.y + bounds.height) +
|
|
105
|
+
'" x2="' +
|
|
106
|
+
formatNumber(bounds.x + bounds.width) +
|
|
107
|
+
'" y2="' +
|
|
108
|
+
formatNumber(bounds.y) +
|
|
109
|
+
'" stroke="' +
|
|
110
|
+
escapeHtml(stroke) +
|
|
111
|
+
'" stroke-width="1" />' +
|
|
112
|
+
'</g>'
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolves one image placement into SVG-space bounds.
|
|
118
|
+
* @param {{ x: number, y: number, cornerX: number, cornerY: number }} image
|
|
119
|
+
* @param {number} sheetHeight
|
|
120
|
+
* @returns {{ x: number, y: number, width: number, height: number }}
|
|
121
|
+
*/
|
|
122
|
+
static #resolveBounds(image, sheetHeight) {
|
|
123
|
+
const minX = Math.min(Number(image.x), Number(image.cornerX))
|
|
124
|
+
const maxX = Math.max(Number(image.x), Number(image.cornerX))
|
|
125
|
+
const minY = Math.min(Number(image.y), Number(image.cornerY))
|
|
126
|
+
const maxY = Math.max(Number(image.y), Number(image.cornerY))
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
x: minX,
|
|
130
|
+
y: projectSchematicY(sheetHeight, maxY),
|
|
131
|
+
width: maxX - minX,
|
|
132
|
+
height: maxY - minY
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
7
|
+
|
|
8
|
+
const { escapeHtml, formatNumber, projectSchematicY } = SchematicSvgUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders synthesized junction dots for connected schematic wire routes.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicJunctionRenderer {
|
|
14
|
+
/**
|
|
15
|
+
* Builds junction-dot markup from connected wire linework.
|
|
16
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, color: string, ownerIndex?: string, isBus?: boolean }[]} lines
|
|
17
|
+
* @param {{ x: number, y: number }[]} crosses
|
|
18
|
+
* @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} [ports]
|
|
19
|
+
* @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' }[]} [powerPorts]
|
|
20
|
+
* @param {number} sheetHeight
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static buildMarkup(
|
|
24
|
+
lines,
|
|
25
|
+
crosses,
|
|
26
|
+
ports = [],
|
|
27
|
+
powerPorts = [],
|
|
28
|
+
sheetHeight
|
|
29
|
+
) {
|
|
30
|
+
return SchematicJunctionRenderer.#resolveJunctions(
|
|
31
|
+
lines,
|
|
32
|
+
crosses,
|
|
33
|
+
ports,
|
|
34
|
+
powerPorts
|
|
35
|
+
)
|
|
36
|
+
.map(
|
|
37
|
+
(junction) =>
|
|
38
|
+
'<circle class="schematic-junction" cx="' +
|
|
39
|
+
formatNumber(junction.x) +
|
|
40
|
+
'" cy="' +
|
|
41
|
+
formatNumber(projectSchematicY(sheetHeight, junction.y)) +
|
|
42
|
+
'" r="2" fill="' +
|
|
43
|
+
escapeHtml(
|
|
44
|
+
SchematicColorResolver.resolveColor(
|
|
45
|
+
junction.color,
|
|
46
|
+
'--schematic-default-ink-color'
|
|
47
|
+
)
|
|
48
|
+
) +
|
|
49
|
+
'" />'
|
|
50
|
+
)
|
|
51
|
+
.join('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves all wire-junction points that should display a connection dot.
|
|
56
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, color: string, ownerIndex?: string, isBus?: boolean }[]} lines
|
|
57
|
+
* @param {{ x: number, y: number }[]} crosses
|
|
58
|
+
* @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} ports
|
|
59
|
+
* @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' }[]} powerPorts
|
|
60
|
+
* @returns {{ x: number, y: number, color: string }[]}
|
|
61
|
+
*/
|
|
62
|
+
static #resolveJunctions(lines, crosses, ports, powerPorts) {
|
|
63
|
+
const wireLines = lines.filter(
|
|
64
|
+
(line) => !line.ownerIndex && line.isBus !== true
|
|
65
|
+
)
|
|
66
|
+
const verticalPorts = ports.filter((port) =>
|
|
67
|
+
SchematicJunctionRenderer.#isVerticalPort(port)
|
|
68
|
+
)
|
|
69
|
+
const visiblePowerPorts = powerPorts.filter((powerPort) =>
|
|
70
|
+
SchematicJunctionRenderer.#isDrawablePowerPort(powerPort)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return SchematicJunctionRenderer.#collectCandidatePoints(
|
|
74
|
+
wireLines,
|
|
75
|
+
verticalPorts,
|
|
76
|
+
visiblePowerPorts
|
|
77
|
+
)
|
|
78
|
+
.filter(
|
|
79
|
+
(point) =>
|
|
80
|
+
!SchematicJunctionRenderer.#hasNearbyCross(point, crosses)
|
|
81
|
+
)
|
|
82
|
+
.flatMap((point) => {
|
|
83
|
+
const contributingLines = wireLines.filter((line) =>
|
|
84
|
+
SchematicJunctionRenderer.#lineContainsPoint(line, point)
|
|
85
|
+
)
|
|
86
|
+
const contributingPorts = verticalPorts.filter((port) =>
|
|
87
|
+
SchematicJunctionRenderer.#portContainsPoint(port, point)
|
|
88
|
+
)
|
|
89
|
+
const contributingPowerPorts = visiblePowerPorts.filter(
|
|
90
|
+
(powerPort) =>
|
|
91
|
+
SchematicJunctionRenderer.#powerPortContainsPoint(
|
|
92
|
+
powerPort,
|
|
93
|
+
point
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
const directions = new Set()
|
|
97
|
+
|
|
98
|
+
for (const line of contributingLines) {
|
|
99
|
+
SchematicJunctionRenderer.#appendDirections(
|
|
100
|
+
directions,
|
|
101
|
+
line,
|
|
102
|
+
point
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const port of contributingPorts) {
|
|
107
|
+
SchematicJunctionRenderer.#appendPortDirections(
|
|
108
|
+
directions,
|
|
109
|
+
port
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const powerPort of contributingPowerPorts) {
|
|
114
|
+
SchematicJunctionRenderer.#appendPowerPortDirections(
|
|
115
|
+
directions,
|
|
116
|
+
powerPort
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (directions.size < 3) {
|
|
121
|
+
return []
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
x: point.x,
|
|
127
|
+
y: point.y,
|
|
128
|
+
color:
|
|
129
|
+
contributingLines[0]?.color ||
|
|
130
|
+
'var(--schematic-default-ink-color)'
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Collects all distinct wire endpoints as candidate junction points.
|
|
138
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
|
|
139
|
+
* @param {{ x: number, y: number, width: number, direction?: 'up' | 'down' }[]} ports
|
|
140
|
+
* @param {{ x: number, y: number }} powerPorts
|
|
141
|
+
* @returns {{ x: number, y: number }[]}
|
|
142
|
+
*/
|
|
143
|
+
static #collectCandidatePoints(lines, ports, powerPorts) {
|
|
144
|
+
const candidates = new Map()
|
|
145
|
+
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
for (const point of [
|
|
148
|
+
{ x: line.x1, y: line.y1 },
|
|
149
|
+
{ x: line.x2, y: line.y2 }
|
|
150
|
+
]) {
|
|
151
|
+
candidates.set(
|
|
152
|
+
SchematicJunctionRenderer.#pointKey(point),
|
|
153
|
+
point
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const port of ports) {
|
|
159
|
+
const connectionPoint =
|
|
160
|
+
SchematicJunctionRenderer.#resolveVerticalPortConnectionPoint(
|
|
161
|
+
port
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
candidates.set(
|
|
165
|
+
SchematicJunctionRenderer.#pointKey(connectionPoint),
|
|
166
|
+
connectionPoint
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const powerPort of powerPorts) {
|
|
171
|
+
candidates.set(
|
|
172
|
+
SchematicJunctionRenderer.#pointKey(powerPort),
|
|
173
|
+
powerPort
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return [...candidates.values()]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Adds all directions a line contributes at one candidate point.
|
|
182
|
+
* @param {Set<string>} directions
|
|
183
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }} line
|
|
184
|
+
* @param {{ x: number, y: number }} point
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
static #appendDirections(directions, line, point) {
|
|
188
|
+
if (line.x1 === line.x2 && line.x1 === point.x) {
|
|
189
|
+
const minY = Math.min(line.y1, line.y2)
|
|
190
|
+
const maxY = Math.max(line.y1, line.y2)
|
|
191
|
+
|
|
192
|
+
if (point.y > minY + 0.01) {
|
|
193
|
+
directions.add('south')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (point.y < maxY - 0.01) {
|
|
197
|
+
directions.add('north')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (line.y1 === line.y2 && line.y1 === point.y) {
|
|
204
|
+
const minX = Math.min(line.x1, line.x2)
|
|
205
|
+
const maxX = Math.max(line.x1, line.x2)
|
|
206
|
+
|
|
207
|
+
if (point.x > minX + 0.01) {
|
|
208
|
+
directions.add('west')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (point.x < maxX - 0.01) {
|
|
212
|
+
directions.add('east')
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Adds the one branch direction contributed by a vertical off-sheet port.
|
|
219
|
+
* @param {Set<string>} directions
|
|
220
|
+
* @param {{ direction?: 'up' | 'down' }} port
|
|
221
|
+
* @returns {void}
|
|
222
|
+
*/
|
|
223
|
+
static #appendPortDirections(directions, port) {
|
|
224
|
+
if (port.direction === 'up') {
|
|
225
|
+
directions.add('south')
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (port.direction === 'down') {
|
|
230
|
+
directions.add('north')
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Adds the branch direction contributed by one rendered power-port symbol.
|
|
236
|
+
* @param {Set<string>} directions
|
|
237
|
+
* @param {{ powerPortDirection?: 'up' | 'down' | 'left' | 'right' }} powerPort
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
static #appendPowerPortDirections(directions, powerPort) {
|
|
241
|
+
switch (powerPort.powerPortDirection) {
|
|
242
|
+
case 'up':
|
|
243
|
+
directions.add('north')
|
|
244
|
+
return
|
|
245
|
+
case 'down':
|
|
246
|
+
directions.add('south')
|
|
247
|
+
return
|
|
248
|
+
case 'left':
|
|
249
|
+
directions.add('west')
|
|
250
|
+
return
|
|
251
|
+
case 'right':
|
|
252
|
+
directions.add('east')
|
|
253
|
+
return
|
|
254
|
+
default:
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Returns true when one cross marker occupies the same point.
|
|
260
|
+
* @param {{ x: number, y: number }} point
|
|
261
|
+
* @param {{ x: number, y: number }[]} crosses
|
|
262
|
+
* @returns {boolean}
|
|
263
|
+
*/
|
|
264
|
+
static #hasNearbyCross(point, crosses) {
|
|
265
|
+
return crosses.some(
|
|
266
|
+
(cross) =>
|
|
267
|
+
Math.abs(cross.x - point.x) <= 0.01 &&
|
|
268
|
+
Math.abs(cross.y - point.y) <= 0.01
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Returns true when one axis-aligned line passes through a candidate point.
|
|
274
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number }} line
|
|
275
|
+
* @param {{ x: number, y: number }} point
|
|
276
|
+
* @returns {boolean}
|
|
277
|
+
*/
|
|
278
|
+
static #lineContainsPoint(line, point) {
|
|
279
|
+
if (line.x1 === line.x2 && line.x1 === point.x) {
|
|
280
|
+
return (
|
|
281
|
+
point.y >= Math.min(line.y1, line.y2) - 0.01 &&
|
|
282
|
+
point.y <= Math.max(line.y1, line.y2) + 0.01
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (line.y1 === line.y2 && line.y1 === point.y) {
|
|
287
|
+
return (
|
|
288
|
+
point.x >= Math.min(line.x1, line.x2) - 0.01 &&
|
|
289
|
+
point.x <= Math.max(line.x1, line.x2) + 0.01
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns true when one vertical off-sheet port attaches at the point.
|
|
298
|
+
* @param {{ x: number, y: number, width: number, direction?: 'up' | 'down' }} port
|
|
299
|
+
* @param {{ x: number, y: number }} point
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
static #portContainsPoint(port, point) {
|
|
303
|
+
if (!SchematicJunctionRenderer.#isVerticalPort(port)) {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const connectionPoint =
|
|
308
|
+
SchematicJunctionRenderer.#resolveVerticalPortConnectionPoint(port)
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
Math.abs(connectionPoint.x - point.x) <= 0.01 &&
|
|
312
|
+
Math.abs(connectionPoint.y - point.y) <= 0.01
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Returns true when one rendered power port attaches at the candidate
|
|
318
|
+
* point.
|
|
319
|
+
* @param {{ x: number, y: number }} powerPort
|
|
320
|
+
* @param {{ x: number, y: number }} point
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
static #powerPortContainsPoint(powerPort, point) {
|
|
324
|
+
return (
|
|
325
|
+
Math.abs(powerPort.x - point.x) <= 0.01 &&
|
|
326
|
+
Math.abs(powerPort.y - point.y) <= 0.01
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Returns the wire attachment point for one vertical off-sheet port.
|
|
332
|
+
* @param {{ x: number, y: number, width: number, direction?: 'up' | 'down' }} port
|
|
333
|
+
* @returns {{ x: number, y: number }}
|
|
334
|
+
*/
|
|
335
|
+
static #resolveVerticalPortConnectionPoint(port) {
|
|
336
|
+
if (port.direction === 'down') {
|
|
337
|
+
return {
|
|
338
|
+
x: port.x,
|
|
339
|
+
y: port.y
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
x: port.x,
|
|
345
|
+
y: port.y + port.width
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Returns true when a port uses the vertical style-4 geometry.
|
|
351
|
+
* @param {{ direction?: 'left' | 'right' | 'up' | 'down' }} port
|
|
352
|
+
* @returns {boolean}
|
|
353
|
+
*/
|
|
354
|
+
static #isVerticalPort(port) {
|
|
355
|
+
return port.direction === 'up' || port.direction === 'down'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Returns true when one visible text-backed power port can contribute a
|
|
360
|
+
* junction branch.
|
|
361
|
+
* @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' } | null | undefined} powerPort
|
|
362
|
+
* @returns {boolean}
|
|
363
|
+
*/
|
|
364
|
+
static #isDrawablePowerPort(powerPort) {
|
|
365
|
+
return (
|
|
366
|
+
Boolean(powerPort) &&
|
|
367
|
+
Number.isFinite(Number(powerPort.x)) &&
|
|
368
|
+
Number.isFinite(Number(powerPort.y)) &&
|
|
369
|
+
typeof powerPort.powerPortDirection === 'string'
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Builds a stable map key for one point.
|
|
375
|
+
* @param {{ x: number, y: number }} point
|
|
376
|
+
* @returns {string}
|
|
377
|
+
*/
|
|
378
|
+
static #pointKey(point) {
|
|
379
|
+
return String(point.x) + ':' + String(point.y)
|
|
380
|
+
}
|
|
381
|
+
}
|