circuitjson-toolkit 1.0.3 → 1.0.16
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 +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders via-like drilled primitives as SVG shapes.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonPcbViaSvgRenderer {
|
|
5
|
+
/**
|
|
6
|
+
* Renders one via-like primitive.
|
|
7
|
+
* @param {object} primitive Via-like primitive.
|
|
8
|
+
* @param {string} attributes Data attributes for the primitive group.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
static render(primitive, attributes = '') {
|
|
12
|
+
const groupAttributes = attributes ? ' ' + attributes : ''
|
|
13
|
+
return (
|
|
14
|
+
'<g class="pcb-via-group"' +
|
|
15
|
+
groupAttributes +
|
|
16
|
+
'>' +
|
|
17
|
+
CircuitJsonPcbViaSvgRenderer.#shape(
|
|
18
|
+
primitive,
|
|
19
|
+
'pcb-via',
|
|
20
|
+
primitive.shape || 'circle',
|
|
21
|
+
primitive.width || primitive.diameter,
|
|
22
|
+
primitive.height || primitive.diameter
|
|
23
|
+
) +
|
|
24
|
+
CircuitJsonPcbViaSvgRenderer.#shape(
|
|
25
|
+
primitive,
|
|
26
|
+
'pcb-via__hole',
|
|
27
|
+
primitive.holeShape || 'circle',
|
|
28
|
+
primitive.holeWidth || primitive.holeDiameter,
|
|
29
|
+
primitive.holeHeight || primitive.holeDiameter
|
|
30
|
+
) +
|
|
31
|
+
'</g>'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders one centered SVG shape.
|
|
37
|
+
* @param {object} primitive Via-like primitive.
|
|
38
|
+
* @param {string} baseClass Base CSS class.
|
|
39
|
+
* @param {string} shape Shape name.
|
|
40
|
+
* @param {number} width Shape width.
|
|
41
|
+
* @param {number} height Shape height.
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
static #shape(primitive, baseClass, shape, width, height) {
|
|
45
|
+
const normalizedShape = String(shape || 'circle').toLowerCase()
|
|
46
|
+
const className =
|
|
47
|
+
baseClass +
|
|
48
|
+
' ' +
|
|
49
|
+
baseClass.replace('__', '-') +
|
|
50
|
+
'--' +
|
|
51
|
+
CircuitJsonPcbViaSvgRenderer.#escapeHtml(normalizedShape)
|
|
52
|
+
|
|
53
|
+
if (normalizedShape === 'circle') {
|
|
54
|
+
return (
|
|
55
|
+
'<circle class="' +
|
|
56
|
+
className +
|
|
57
|
+
'" cx="' +
|
|
58
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(primitive.x) +
|
|
59
|
+
'" cy="' +
|
|
60
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(primitive.y) +
|
|
61
|
+
'" r="' +
|
|
62
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(width / 2) +
|
|
63
|
+
'"></circle>'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
normalizedShape === 'polygon' &&
|
|
69
|
+
Array.isArray(primitive.points) &&
|
|
70
|
+
primitive.points.length >= 3
|
|
71
|
+
) {
|
|
72
|
+
return (
|
|
73
|
+
'<polygon class="' +
|
|
74
|
+
className +
|
|
75
|
+
'" points="' +
|
|
76
|
+
CircuitJsonPcbViaSvgRenderer.#escapeHtml(
|
|
77
|
+
CircuitJsonPcbViaSvgRenderer.#pointsAttribute(
|
|
78
|
+
primitive.points
|
|
79
|
+
)
|
|
80
|
+
) +
|
|
81
|
+
'"></polygon>'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
'<rect class="' +
|
|
87
|
+
className +
|
|
88
|
+
'" x="' +
|
|
89
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(
|
|
90
|
+
primitive.x - width / 2
|
|
91
|
+
) +
|
|
92
|
+
'" y="' +
|
|
93
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(
|
|
94
|
+
primitive.y - height / 2
|
|
95
|
+
) +
|
|
96
|
+
'" width="' +
|
|
97
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(width) +
|
|
98
|
+
'" height="' +
|
|
99
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(height) +
|
|
100
|
+
'" rx="' +
|
|
101
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(
|
|
102
|
+
normalizedShape === 'pill' ? Math.min(width, height) / 2 : 0
|
|
103
|
+
) +
|
|
104
|
+
'"' +
|
|
105
|
+
CircuitJsonPcbViaSvgRenderer.#rotationAttribute(primitive) +
|
|
106
|
+
'></rect>'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Builds a rotation transform attribute.
|
|
112
|
+
* @param {object} primitive Via-like primitive.
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
static #rotationAttribute(primitive) {
|
|
116
|
+
const rotation = Number(primitive.rotation || 0)
|
|
117
|
+
if (!Number.isFinite(rotation) || rotation === 0) return ''
|
|
118
|
+
return (
|
|
119
|
+
' transform="rotate(' +
|
|
120
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(rotation) +
|
|
121
|
+
' ' +
|
|
122
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(primitive.x) +
|
|
123
|
+
' ' +
|
|
124
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(primitive.y) +
|
|
125
|
+
')"'
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Builds a polygon points attribute.
|
|
131
|
+
* @param {{ x: number, y: number }[]} points Point rows.
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
static #pointsAttribute(points) {
|
|
135
|
+
return points
|
|
136
|
+
.map(
|
|
137
|
+
(point) =>
|
|
138
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(point.x) +
|
|
139
|
+
',' +
|
|
140
|
+
CircuitJsonPcbViaSvgRenderer.#formatNumber(point.y)
|
|
141
|
+
)
|
|
142
|
+
.join(' ')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Formats a number for SVG output.
|
|
147
|
+
* @param {number} value Numeric value.
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
static #formatNumber(value) {
|
|
151
|
+
const number = Number(value)
|
|
152
|
+
if (!Number.isFinite(number)) return '0'
|
|
153
|
+
return String(Math.round(number * 1_000_000) / 1_000_000)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Escapes text for HTML attributes.
|
|
158
|
+
* @param {unknown} value Raw value.
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
static #escapeHtml(value) {
|
|
162
|
+
return String(value ?? '')
|
|
163
|
+
.replaceAll('&', '&')
|
|
164
|
+
.replaceAll('"', '"')
|
|
165
|
+
.replaceAll('<', '<')
|
|
166
|
+
.replaceAll('>', '>')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds SVG path data for schematic arcs.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonSchematicSvgArcPath {
|
|
5
|
+
/**
|
|
6
|
+
* Builds arc path data from start, mid, and end points.
|
|
7
|
+
* @param {{ x: number, y: number } | null} start Start point.
|
|
8
|
+
* @param {{ x: number, y: number } | null} mid Midpoint on the arc.
|
|
9
|
+
* @param {{ x: number, y: number } | null} end End point.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
static fromThreePoints(start, mid, end) {
|
|
13
|
+
if (!start || !mid || !end) return ''
|
|
14
|
+
const circle = CircuitJsonSchematicSvgArcPath.#circle(start, mid, end)
|
|
15
|
+
if (!circle) {
|
|
16
|
+
return (
|
|
17
|
+
'M ' +
|
|
18
|
+
CircuitJsonSchematicSvgArcPath.#formatPoint(start) +
|
|
19
|
+
' L ' +
|
|
20
|
+
CircuitJsonSchematicSvgArcPath.#formatPoint(end)
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
const startAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, start)
|
|
24
|
+
const midAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, mid)
|
|
25
|
+
const endAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, end)
|
|
26
|
+
const clockwiseSpan = CircuitJsonSchematicSvgArcPath.#positiveDelta(
|
|
27
|
+
startAngle,
|
|
28
|
+
endAngle
|
|
29
|
+
)
|
|
30
|
+
const clockwiseMid = CircuitJsonSchematicSvgArcPath.#positiveDelta(
|
|
31
|
+
startAngle,
|
|
32
|
+
midAngle
|
|
33
|
+
)
|
|
34
|
+
const sweep = clockwiseMid <= clockwiseSpan + 0.000001 ? 1 : 0
|
|
35
|
+
const span =
|
|
36
|
+
sweep === 1
|
|
37
|
+
? clockwiseSpan
|
|
38
|
+
: CircuitJsonSchematicSvgArcPath.#positiveDelta(
|
|
39
|
+
endAngle,
|
|
40
|
+
startAngle
|
|
41
|
+
)
|
|
42
|
+
const largeArc = span > Math.PI ? 1 : 0
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
'M ' +
|
|
46
|
+
CircuitJsonSchematicSvgArcPath.#formatPoint(start) +
|
|
47
|
+
' A ' +
|
|
48
|
+
CircuitJsonSchematicSvgArcPath.#formatNumber(circle.radius) +
|
|
49
|
+
' ' +
|
|
50
|
+
CircuitJsonSchematicSvgArcPath.#formatNumber(circle.radius) +
|
|
51
|
+
' 0 ' +
|
|
52
|
+
largeArc +
|
|
53
|
+
' ' +
|
|
54
|
+
sweep +
|
|
55
|
+
' ' +
|
|
56
|
+
CircuitJsonSchematicSvgArcPath.#formatPoint(end)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the circle through three points.
|
|
62
|
+
* @param {{ x: number, y: number }} start Start point.
|
|
63
|
+
* @param {{ x: number, y: number }} mid Midpoint.
|
|
64
|
+
* @param {{ x: number, y: number }} end End point.
|
|
65
|
+
* @returns {{ x: number, y: number, radius: number } | null}
|
|
66
|
+
*/
|
|
67
|
+
static #circle(start, mid, end) {
|
|
68
|
+
const denominator =
|
|
69
|
+
2 *
|
|
70
|
+
(start.x * (mid.y - end.y) +
|
|
71
|
+
mid.x * (end.y - start.y) +
|
|
72
|
+
end.x * (start.y - mid.y))
|
|
73
|
+
if (Math.abs(denominator) < 0.000001) return null
|
|
74
|
+
|
|
75
|
+
const startLength = start.x * start.x + start.y * start.y
|
|
76
|
+
const midLength = mid.x * mid.x + mid.y * mid.y
|
|
77
|
+
const endLength = end.x * end.x + end.y * end.y
|
|
78
|
+
const x =
|
|
79
|
+
(startLength * (mid.y - end.y) +
|
|
80
|
+
midLength * (end.y - start.y) +
|
|
81
|
+
endLength * (start.y - mid.y)) /
|
|
82
|
+
denominator
|
|
83
|
+
const y =
|
|
84
|
+
(startLength * (end.x - mid.x) +
|
|
85
|
+
midLength * (start.x - end.x) +
|
|
86
|
+
endLength * (mid.x - start.x)) /
|
|
87
|
+
denominator
|
|
88
|
+
const radius = Math.hypot(start.x - x, start.y - y)
|
|
89
|
+
|
|
90
|
+
return Number.isFinite(radius) ? { x, y, radius } : null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the angle from a circle center to a point.
|
|
95
|
+
* @param {{ x: number, y: number }} center Circle center.
|
|
96
|
+
* @param {{ x: number, y: number }} point Point on circle.
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
static #angle(center, point) {
|
|
100
|
+
return Math.atan2(point.y - center.y, point.x - center.x)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves the positive angular delta from start to end.
|
|
105
|
+
* @param {number} start Start angle.
|
|
106
|
+
* @param {number} end End angle.
|
|
107
|
+
* @returns {number}
|
|
108
|
+
*/
|
|
109
|
+
static #positiveDelta(start, end) {
|
|
110
|
+
const full = Math.PI * 2
|
|
111
|
+
return (((end - start) % full) + full) % full
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Formats one point for SVG path data.
|
|
116
|
+
* @param {{ x: number, y: number }} point Point.
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
static #formatPoint(point) {
|
|
120
|
+
return (
|
|
121
|
+
CircuitJsonSchematicSvgArcPath.#formatNumber(point.x) +
|
|
122
|
+
' ' +
|
|
123
|
+
CircuitJsonSchematicSvgArcPath.#formatNumber(point.y)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Formats one SVG number.
|
|
129
|
+
* @param {number} value Number value.
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
static #formatNumber(value) {
|
|
133
|
+
const number = Number(value)
|
|
134
|
+
const integer = Math.round(number)
|
|
135
|
+
if (Math.abs(number - integer) < 0.000001) return String(integer)
|
|
136
|
+
return Number(number.toFixed(6)).toString()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves schematic port display metadata from linked source ports.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonSchematicSvgPortMetadata {
|
|
5
|
+
/**
|
|
6
|
+
* Builds source port lookup rows.
|
|
7
|
+
* @param {object[]} sourcePorts Source port rows.
|
|
8
|
+
* @returns {Map<string, object>}
|
|
9
|
+
*/
|
|
10
|
+
static sourcePorts(sourcePorts) {
|
|
11
|
+
return new Map(
|
|
12
|
+
sourcePorts
|
|
13
|
+
.map((element) => [
|
|
14
|
+
String(element.source_port_id || '').trim(),
|
|
15
|
+
element
|
|
16
|
+
])
|
|
17
|
+
.filter(([sourcePortId]) => sourcePortId)
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds the schematic port group attributes.
|
|
23
|
+
* @param {object} element Schematic port row.
|
|
24
|
+
* @param {object | null} sourcePort Linked source port row.
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
static attributes(element, sourcePort) {
|
|
28
|
+
const sourcePortId = String(element.source_port_id || '').trim()
|
|
29
|
+
const pinNumber = CircuitJsonSchematicSvgPortMetadata.#firstText(
|
|
30
|
+
element.pin_number,
|
|
31
|
+
sourcePort?.pin_number
|
|
32
|
+
)
|
|
33
|
+
const direction = String(
|
|
34
|
+
element.facing_direction || element.direction || ''
|
|
35
|
+
).trim()
|
|
36
|
+
const attributes = [
|
|
37
|
+
'class="schematic-port"',
|
|
38
|
+
'data-schematic-port-id="' +
|
|
39
|
+
CircuitJsonSchematicSvgPortMetadata.#escapeHtml(
|
|
40
|
+
element.schematic_port_id || ''
|
|
41
|
+
) +
|
|
42
|
+
'"',
|
|
43
|
+
'data-facing-direction="' +
|
|
44
|
+
CircuitJsonSchematicSvgPortMetadata.#escapeHtml(direction) +
|
|
45
|
+
'"'
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
if (sourcePortId) {
|
|
49
|
+
attributes.push(
|
|
50
|
+
'data-source-port-id="' +
|
|
51
|
+
CircuitJsonSchematicSvgPortMetadata.#escapeHtml(
|
|
52
|
+
sourcePortId
|
|
53
|
+
) +
|
|
54
|
+
'"'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (pinNumber) {
|
|
58
|
+
attributes.push(
|
|
59
|
+
'data-pin-number="' +
|
|
60
|
+
CircuitJsonSchematicSvgPortMetadata.#escapeHtml(pinNumber) +
|
|
61
|
+
'"'
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return attributes.join(' ')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves a visible schematic port label.
|
|
70
|
+
* @param {object} element Schematic port row.
|
|
71
|
+
* @param {object | null} sourcePort Linked source port row.
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
static label(element, sourcePort) {
|
|
75
|
+
return CircuitJsonSchematicSvgPortMetadata.#firstText(
|
|
76
|
+
element.name,
|
|
77
|
+
element.label,
|
|
78
|
+
sourcePort?.name,
|
|
79
|
+
sourcePort?.label,
|
|
80
|
+
sourcePort?.pin_label,
|
|
81
|
+
element.pin_number,
|
|
82
|
+
sourcePort?.pin_number,
|
|
83
|
+
...(Array.isArray(sourcePort?.port_hints)
|
|
84
|
+
? sourcePort.port_hints
|
|
85
|
+
: [])
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the first non-empty display text value.
|
|
91
|
+
* @param {...unknown} values Candidate values.
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
static #firstText(...values) {
|
|
95
|
+
return (
|
|
96
|
+
values
|
|
97
|
+
.map((value) => String(value ?? '').trim())
|
|
98
|
+
.find((value) => value) || ''
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Escapes markup text.
|
|
104
|
+
* @param {unknown} value Raw value.
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
static #escapeHtml(value) {
|
|
108
|
+
return String(value ?? '')
|
|
109
|
+
.replaceAll('&', '&')
|
|
110
|
+
.replaceAll('<', '<')
|
|
111
|
+
.replaceAll('>', '>')
|
|
112
|
+
.replaceAll('"', '"')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds SVG drawing attributes for schematic primitive rows.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonSchematicSvgPrimitiveAttributes {
|
|
5
|
+
/**
|
|
6
|
+
* Builds optional SVG attributes from common drawing style fields.
|
|
7
|
+
* @param {object} element CircuitJSON element row.
|
|
8
|
+
* @param {{ fill?: boolean }} [options] Attribute options.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
static attributes(element, options = {}) {
|
|
12
|
+
const attributes = []
|
|
13
|
+
const strokeWidth = CircuitJsonSchematicSvgPrimitiveAttributes.#number(
|
|
14
|
+
element?.stroke_width ?? element?.strokeWidth
|
|
15
|
+
)
|
|
16
|
+
const strokeColor = CircuitJsonSchematicSvgPrimitiveAttributes.#text(
|
|
17
|
+
element?.stroke_color ?? element?.strokeColor ?? element?.color
|
|
18
|
+
)
|
|
19
|
+
const fillColor = CircuitJsonSchematicSvgPrimitiveAttributes.#text(
|
|
20
|
+
element?.fill_color ?? element?.fillColor
|
|
21
|
+
)
|
|
22
|
+
const dashArray =
|
|
23
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#dashArray(element)
|
|
24
|
+
|
|
25
|
+
if (strokeWidth !== null) {
|
|
26
|
+
attributes.push(
|
|
27
|
+
'stroke-width="' +
|
|
28
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#formatNumber(
|
|
29
|
+
strokeWidth
|
|
30
|
+
) +
|
|
31
|
+
'"'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (strokeColor) {
|
|
35
|
+
attributes.push(
|
|
36
|
+
'stroke="' +
|
|
37
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#escapeHtml(
|
|
38
|
+
strokeColor
|
|
39
|
+
) +
|
|
40
|
+
'"'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
if (options.fill !== false && fillColor) {
|
|
44
|
+
attributes.push(
|
|
45
|
+
'fill="' +
|
|
46
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#escapeHtml(
|
|
47
|
+
fillColor
|
|
48
|
+
) +
|
|
49
|
+
'"'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
if (dashArray) {
|
|
53
|
+
attributes.push(
|
|
54
|
+
'stroke-dasharray="' +
|
|
55
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#escapeHtml(
|
|
56
|
+
dashArray
|
|
57
|
+
) +
|
|
58
|
+
'"'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return attributes.length ? ' ' + attributes.join(' ') : ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a finite number.
|
|
67
|
+
* @param {unknown} value Number candidate.
|
|
68
|
+
* @returns {number | null}
|
|
69
|
+
*/
|
|
70
|
+
static #number(value) {
|
|
71
|
+
const number = Number(value)
|
|
72
|
+
return Number.isFinite(number) ? number : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves non-empty text.
|
|
77
|
+
* @param {unknown} value Text candidate.
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
static #text(value) {
|
|
81
|
+
return String(value ?? '').trim()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves a stroke dash array.
|
|
86
|
+
* @param {object} element CircuitJSON element row.
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
static #dashArray(element) {
|
|
90
|
+
const value =
|
|
91
|
+
element?.stroke_dasharray ??
|
|
92
|
+
element?.strokeDasharray ??
|
|
93
|
+
element?.dash_pattern ??
|
|
94
|
+
element?.dashPattern
|
|
95
|
+
const values = Array.isArray(value) ? value : [value]
|
|
96
|
+
const parts = values
|
|
97
|
+
.map((entry) =>
|
|
98
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#number(entry)
|
|
99
|
+
)
|
|
100
|
+
.filter((entry) => entry !== null)
|
|
101
|
+
.map((entry) =>
|
|
102
|
+
CircuitJsonSchematicSvgPrimitiveAttributes.#formatNumber(entry)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (parts.length) return parts.join(' ')
|
|
106
|
+
return element?.is_dashed ? '0.4 0.25' : ''
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Formats one SVG number.
|
|
111
|
+
* @param {number} value Number value.
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
static #formatNumber(value) {
|
|
115
|
+
return Number(Number(value).toFixed(6)).toString()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Escapes markup text.
|
|
120
|
+
* @param {unknown} value Raw value.
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
static #escapeHtml(value) {
|
|
124
|
+
return String(value ?? '')
|
|
125
|
+
.replaceAll('&', '&')
|
|
126
|
+
.replaceAll('<', '<')
|
|
127
|
+
.replaceAll('>', '>')
|
|
128
|
+
.replaceAll('"', '"')
|
|
129
|
+
}
|
|
130
|
+
}
|