altium-toolkit 1.4.11 → 1.4.13
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.
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# altium-toolkit 1.4.12
|
|
2
|
+
|
|
3
|
+
Version 1.4.12 preserves native schematic footer metadata when a project also
|
|
4
|
+
provides document-level special-string parameters.
|
|
5
|
+
|
|
6
|
+
## Project schematic fidelity
|
|
7
|
+
|
|
8
|
+
- Project parameters are resolved before the convergence fidelity pass so
|
|
9
|
+
document context and native fallback metadata compose deterministically.
|
|
10
|
+
- Footer fallback values synchronize the visible text, resolved-text sidecar,
|
|
11
|
+
and special-string expression before the historical renderer consumes them.
|
|
12
|
+
- Organization, address, approval, and other native owner metadata no longer
|
|
13
|
+
regress to unresolved placeholders in complete project loads.
|
|
14
|
+
|
|
15
|
+
## Compatibility
|
|
16
|
+
|
|
17
|
+
- Historical parser and renderer sources remain byte-for-byte frozen.
|
|
18
|
+
- Existing schematic colors, canvas borders, title-block chrome, and public
|
|
19
|
+
renderer signatures remain unchanged.
|
|
20
|
+
- Resolution derives from the ownership and parameter data models without
|
|
21
|
+
project names, filenames, labels, or sample-specific rules.
|
|
22
|
+
|
|
23
|
+
## Verification
|
|
24
|
+
|
|
25
|
+
- A repository-owned generic regression covers project parameters combined
|
|
26
|
+
with native footer metadata fallbacks.
|
|
27
|
+
- The complete package suite, immutable-source checks, feature-preservation
|
|
28
|
+
check, formatting check, performance guard, and npm package dry run are
|
|
29
|
+
required for release.
|
package/package.json
CHANGED
|
@@ -50,11 +50,18 @@ export class AltiumSchematicFidelityNormalizer {
|
|
|
50
50
|
schematic.harnesses,
|
|
51
51
|
records
|
|
52
52
|
)
|
|
53
|
+
const symbolPrimitives =
|
|
54
|
+
AltiumSchematicFidelityNormalizer.#normalizeSymbolPrimitives(
|
|
55
|
+
schematic
|
|
56
|
+
)
|
|
53
57
|
|
|
54
58
|
if (
|
|
55
59
|
sheet === schematic.sheet &&
|
|
56
60
|
texts === schematic.texts &&
|
|
57
|
-
harnesses === schematic.harnesses
|
|
61
|
+
harnesses === schematic.harnesses &&
|
|
62
|
+
symbolPrimitives.lines === schematic.lines &&
|
|
63
|
+
symbolPrimitives.rectangles === schematic.rectangles &&
|
|
64
|
+
symbolPrimitives.roundedRectangles === schematic.roundedRectangles
|
|
58
65
|
) {
|
|
59
66
|
return documentModel
|
|
60
67
|
}
|
|
@@ -63,6 +70,7 @@ export class AltiumSchematicFidelityNormalizer {
|
|
|
63
70
|
...documentModel,
|
|
64
71
|
schematic: {
|
|
65
72
|
...schematic,
|
|
73
|
+
...symbolPrimitives,
|
|
66
74
|
sheet,
|
|
67
75
|
texts,
|
|
68
76
|
...(harnesses ? { harnesses } : {})
|
|
@@ -70,6 +78,122 @@ export class AltiumSchematicFidelityNormalizer {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Themes geometry owned by pin-bearing symbol groups while retaining
|
|
83
|
+
* pinless decorative source-color strips.
|
|
84
|
+
* @param {Record<string, any>} schematic Native schematic model.
|
|
85
|
+
* @returns {{ lines: object[], rectangles: object[], roundedRectangles: object[] }} Normalized primitive collections.
|
|
86
|
+
*/
|
|
87
|
+
static #normalizeSymbolPrimitives(schematic) {
|
|
88
|
+
const pinOwners = new Set(
|
|
89
|
+
(schematic.pins || [])
|
|
90
|
+
.map((pin) => String(pin?.ownerIndex || '').trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (!pinOwners.size) {
|
|
95
|
+
return {
|
|
96
|
+
lines: schematic.lines || [],
|
|
97
|
+
rectangles: schematic.rectangles || [],
|
|
98
|
+
roundedRectangles: schematic.roundedRectangles || []
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
lines: AltiumSchematicFidelityNormalizer.#mapChanged(
|
|
104
|
+
schematic.lines || [],
|
|
105
|
+
(line) =>
|
|
106
|
+
pinOwners.has(String(line?.ownerIndex || '').trim())
|
|
107
|
+
? {
|
|
108
|
+
...line,
|
|
109
|
+
color: 'var(--schematic-power-color)'
|
|
110
|
+
}
|
|
111
|
+
: line
|
|
112
|
+
),
|
|
113
|
+
rectangles: AltiumSchematicFidelityNormalizer.#mapChanged(
|
|
114
|
+
schematic.rectangles || [],
|
|
115
|
+
(rectangle) =>
|
|
116
|
+
AltiumSchematicFidelityNormalizer.#themeSymbolRectangle(
|
|
117
|
+
rectangle,
|
|
118
|
+
pinOwners
|
|
119
|
+
)
|
|
120
|
+
),
|
|
121
|
+
roundedRectangles: AltiumSchematicFidelityNormalizer.#mapChanged(
|
|
122
|
+
schematic.roundedRectangles || [],
|
|
123
|
+
(rectangle) =>
|
|
124
|
+
AltiumSchematicFidelityNormalizer.#themeSymbolRectangle(
|
|
125
|
+
rectangle,
|
|
126
|
+
pinOwners
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolves one pin-bearing symbol rectangle to shared theme roles.
|
|
134
|
+
* @param {Record<string, any>} rectangle Rectangle primitive.
|
|
135
|
+
* @param {Set<string>} pinOwners Pin-bearing owner keys.
|
|
136
|
+
* @returns {Record<string, any>} Original or themed rectangle.
|
|
137
|
+
*/
|
|
138
|
+
static #themeSymbolRectangle(rectangle, pinOwners) {
|
|
139
|
+
const owner = String(rectangle?.ownerIndex || '').trim()
|
|
140
|
+
if (!owner || !pinOwners.has(owner)) return rectangle
|
|
141
|
+
|
|
142
|
+
const isSolid = rectangle?.isSolid === true
|
|
143
|
+
const isContact =
|
|
144
|
+
isSolid &&
|
|
145
|
+
AltiumSchematicFidelityNormalizer.#isNarrowSymbolContact(rectangle)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...rectangle,
|
|
149
|
+
color: 'var(--schematic-power-color)',
|
|
150
|
+
...(isSolid
|
|
151
|
+
? {
|
|
152
|
+
fill: isContact
|
|
153
|
+
? 'var(--schematic-power-color)'
|
|
154
|
+
: 'var(--schematic-fill-color)',
|
|
155
|
+
transparent: false
|
|
156
|
+
}
|
|
157
|
+
: {})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns true when a solid symbol rectangle encodes a contact bar.
|
|
163
|
+
* @param {{ width?: number, height?: number }} rectangle Rectangle primitive.
|
|
164
|
+
* @returns {boolean} Whether the rectangle is a narrow contact.
|
|
165
|
+
*/
|
|
166
|
+
static #isNarrowSymbolContact(rectangle) {
|
|
167
|
+
const width = Math.abs(Number(rectangle?.width || 0))
|
|
168
|
+
const height = Math.abs(Number(rectangle?.height || 0))
|
|
169
|
+
const shortSide = Math.min(width, height)
|
|
170
|
+
const longSide = Math.max(width, height)
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
shortSide > 0 &&
|
|
174
|
+
shortSide <= 6 &&
|
|
175
|
+
longSide >= 12 &&
|
|
176
|
+
longSide >= shortSide * 2.5
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Maps a collection while retaining its identity when no row changes.
|
|
182
|
+
* @param {object[]} values Source rows.
|
|
183
|
+
* @param {(value: object) => object} mapper Row mapper.
|
|
184
|
+
* @returns {object[]} Original or mapped collection.
|
|
185
|
+
*/
|
|
186
|
+
static #mapChanged(values, mapper) {
|
|
187
|
+
let changed = false
|
|
188
|
+
const mapped = values.map((value) => {
|
|
189
|
+
const next = mapper(value)
|
|
190
|
+
changed ||= next !== value
|
|
191
|
+
return next
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return changed ? mapped : values
|
|
195
|
+
}
|
|
196
|
+
|
|
73
197
|
/**
|
|
74
198
|
* Restores a proven embedded native frame from source sheet dimensions.
|
|
75
199
|
* @param {Record<string, any>} schematic Native schematic model.
|
|
@@ -214,12 +338,47 @@ export class AltiumSchematicFidelityNormalizer {
|
|
|
214
338
|
if (!replacement) return text
|
|
215
339
|
|
|
216
340
|
changed = true
|
|
217
|
-
return
|
|
341
|
+
return AltiumSchematicFidelityNormalizer.#resolvedFooterText(
|
|
342
|
+
text,
|
|
343
|
+
replacement
|
|
344
|
+
)
|
|
218
345
|
})
|
|
219
346
|
|
|
220
347
|
return changed ? resolvedTexts : texts
|
|
221
348
|
}
|
|
222
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Synchronizes one footer fallback across the visible text sidecar fields.
|
|
352
|
+
* @param {Record<string, any>} text Footer text primitive.
|
|
353
|
+
* @param {string} replacement Resolved native metadata value.
|
|
354
|
+
* @returns {Record<string, any>} Resolved footer text primitive.
|
|
355
|
+
*/
|
|
356
|
+
static #resolvedFooterText(text, replacement) {
|
|
357
|
+
const specialString = text?.specialString
|
|
358
|
+
const resolvedSpecialString = specialString
|
|
359
|
+
? {
|
|
360
|
+
...specialString,
|
|
361
|
+
resolvedText: replacement,
|
|
362
|
+
expressionParts: Array.isArray(specialString.expressionParts)
|
|
363
|
+
? specialString.expressionParts.map((part) =>
|
|
364
|
+
part?.kind === 'parameter'
|
|
365
|
+
? { ...part, value: replacement }
|
|
366
|
+
: part
|
|
367
|
+
)
|
|
368
|
+
: specialString.expressionParts
|
|
369
|
+
}
|
|
370
|
+
: specialString
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
...text,
|
|
374
|
+
text: replacement,
|
|
375
|
+
resolvedText: replacement,
|
|
376
|
+
...(resolvedSpecialString
|
|
377
|
+
? { specialString: resolvedSpecialString }
|
|
378
|
+
: {})
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
223
382
|
/**
|
|
224
383
|
* Returns true when an ownership record seeds the lower-right footer.
|
|
225
384
|
* @param {Record<string, any>} record Ownership record.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
import { SchematicSvgRenderer as LegacySchematicSvgRenderer } from '../ui/SchematicSvgRenderer.mjs'
|
|
5
|
+
import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
|
|
5
6
|
import { AltiumSchematicImageNormalizer } from './AltiumSchematicImageNormalizer.mjs'
|
|
6
7
|
import { AltiumSchematicNativeFooterOwnerAligner } from './AltiumSchematicNativeFooterOwnerAligner.mjs'
|
|
7
8
|
import { AltiumSchematicFidelityNormalizer } from './AltiumSchematicFidelityNormalizer.mjs'
|
|
@@ -21,17 +22,32 @@ export class SchematicSvgRenderer {
|
|
|
21
22
|
* @returns {string} Rendered SVG panel markup.
|
|
22
23
|
*/
|
|
23
24
|
static render(documentModel, options = {}) {
|
|
25
|
+
const projectParameters = options.projectParameters
|
|
26
|
+
const projectResolved = projectParameters
|
|
27
|
+
? SchematicProjectParameterResolver.applyToDocumentModel(
|
|
28
|
+
documentModel,
|
|
29
|
+
projectParameters,
|
|
30
|
+
{ replaceText: true }
|
|
31
|
+
)
|
|
32
|
+
: documentModel
|
|
24
33
|
const normalized =
|
|
25
|
-
AltiumSchematicImageNormalizer.normalize(
|
|
34
|
+
AltiumSchematicImageNormalizer.normalize(projectResolved)
|
|
26
35
|
const fidelityNormalized =
|
|
27
36
|
AltiumSchematicFidelityNormalizer.normalize(normalized)
|
|
28
37
|
const aligned =
|
|
29
38
|
AltiumSchematicNativeFooterOwnerAligner.align(fidelityNormalized)
|
|
30
39
|
const renderDocument =
|
|
31
40
|
SchematicSvgRenderer.#visibilityAwareDocument(aligned)
|
|
41
|
+
const legacyOptions = projectParameters
|
|
42
|
+
? Object.fromEntries(
|
|
43
|
+
Object.entries(options).filter(
|
|
44
|
+
([name]) => name !== 'projectParameters'
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
: options
|
|
32
48
|
const markup = LegacySchematicSvgRenderer.render(
|
|
33
49
|
renderDocument,
|
|
34
|
-
|
|
50
|
+
legacyOptions
|
|
35
51
|
)
|
|
36
52
|
|
|
37
53
|
return SchematicSvgRenderer.#injectHarnessMarkup(markup, renderDocument)
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
6
|
import { SchematicTypography } from './SchematicTypography.mjs'
|
|
7
|
-
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
8
7
|
|
|
9
8
|
const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
|
|
10
9
|
SchematicSvgUtils
|
|
@@ -49,34 +48,101 @@ export class SchematicHarnessRenderer {
|
|
|
49
48
|
* @returns {string}
|
|
50
49
|
*/
|
|
51
50
|
static #buildSignalHarnessMarkup(signalHarness, sheetHeight) {
|
|
52
|
-
const
|
|
53
|
-
.map(
|
|
54
|
-
(point)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const projectedPoints = (signalHarness.points || [])
|
|
52
|
+
.map((point) => ({
|
|
53
|
+
x: Number(point?.x),
|
|
54
|
+
y: projectSchematicY(sheetHeight, Number(point?.y))
|
|
55
|
+
}))
|
|
56
|
+
.filter(
|
|
57
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
58
58
|
)
|
|
59
|
+
const points = projectedPoints
|
|
60
|
+
.map((point) => formatNumber(point.x) + ',' + formatNumber(point.y))
|
|
59
61
|
.join(' ')
|
|
60
62
|
|
|
61
|
-
if (
|
|
63
|
+
if (projectedPoints.length < 2) return ''
|
|
64
|
+
|
|
65
|
+
const railWidth = Math.max(
|
|
66
|
+
(Number(signalHarness.lineWidth) || 1) * 4,
|
|
67
|
+
8
|
|
68
|
+
)
|
|
62
69
|
|
|
63
70
|
return (
|
|
64
|
-
'<
|
|
71
|
+
'<g class="schematic-signal-harness">' +
|
|
72
|
+
'<polyline class="schematic-signal-harness__outline" points="' +
|
|
65
73
|
escapeHtml(points) +
|
|
66
|
-
'" fill="none" stroke="' +
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
'" fill="none" stroke="var(--schematic-accent-ink-color)" stroke-opacity="0.28" stroke-width="' +
|
|
75
|
+
formatNumber(railWidth + 2) +
|
|
76
|
+
'" stroke-linecap="round" stroke-linejoin="round" />' +
|
|
77
|
+
'<polyline class="schematic-signal-harness__rail" points="' +
|
|
78
|
+
escapeHtml(points) +
|
|
79
|
+
'" fill="none" stroke="var(--schematic-pin-marker-fill)" stroke-width="' +
|
|
80
|
+
formatNumber(railWidth) +
|
|
81
|
+
'" stroke-linecap="round" stroke-linejoin="round" />' +
|
|
82
|
+
SchematicHarnessRenderer.#buildSignalHarnessMarks(
|
|
83
|
+
projectedPoints,
|
|
84
|
+
railWidth
|
|
73
85
|
) +
|
|
74
|
-
'
|
|
75
|
-
formatNumber(Math.max(Number(signalHarness.lineWidth) || 1, 1)) +
|
|
76
|
-
'" stroke-linecap="round" stroke-linejoin="round" />'
|
|
86
|
+
'</g>'
|
|
77
87
|
)
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Builds repeated diagonal marks along every projected harness segment.
|
|
92
|
+
* @param {{ x: number, y: number }[]} points Projected harness points.
|
|
93
|
+
* @param {number} railWidth Rendered harness width.
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
static #buildSignalHarnessMarks(points, railWidth) {
|
|
97
|
+
const marks = []
|
|
98
|
+
const spacing = Math.max(railWidth * 1.45, 10)
|
|
99
|
+
|
|
100
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
101
|
+
const start = points[index - 1]
|
|
102
|
+
const end = points[index]
|
|
103
|
+
const deltaX = end.x - start.x
|
|
104
|
+
const deltaY = end.y - start.y
|
|
105
|
+
const length = Math.hypot(deltaX, deltaY)
|
|
106
|
+
|
|
107
|
+
if (!Number.isFinite(length) || length <= 0.001) continue
|
|
108
|
+
|
|
109
|
+
const tangentX = deltaX / length
|
|
110
|
+
const tangentY = deltaY / length
|
|
111
|
+
const normalX = -tangentY
|
|
112
|
+
const normalY = tangentX
|
|
113
|
+
const halfAlong = railWidth * 0.32
|
|
114
|
+
const halfNormal = railWidth * 0.42
|
|
115
|
+
const markCount = Math.min(
|
|
116
|
+
Math.max(Math.floor(length / spacing), 1),
|
|
117
|
+
4096
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for (let markIndex = 0; markIndex < markCount; markIndex += 1) {
|
|
121
|
+
const distance = ((markIndex + 0.5) * length) / markCount
|
|
122
|
+
const centerX = start.x + tangentX * distance
|
|
123
|
+
const centerY = start.y + tangentY * distance
|
|
124
|
+
const x1 = centerX - tangentX * halfAlong - normalX * halfNormal
|
|
125
|
+
const y1 = centerY - tangentY * halfAlong - normalY * halfNormal
|
|
126
|
+
const x2 = centerX + tangentX * halfAlong + normalX * halfNormal
|
|
127
|
+
const y2 = centerY + tangentY * halfAlong + normalY * halfNormal
|
|
128
|
+
|
|
129
|
+
marks.push(
|
|
130
|
+
'<line class="schematic-signal-harness__mark" x1="' +
|
|
131
|
+
formatNumber(x1) +
|
|
132
|
+
'" y1="' +
|
|
133
|
+
formatNumber(y1) +
|
|
134
|
+
'" x2="' +
|
|
135
|
+
formatNumber(x2) +
|
|
136
|
+
'" y2="' +
|
|
137
|
+
formatNumber(y2) +
|
|
138
|
+
'" stroke="var(--schematic-accent-ink-color)" stroke-opacity="0.32" stroke-width="1.4" stroke-linecap="round" />'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return marks.join('')
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
/**
|
|
81
147
|
* Builds one harness connector with its entry labels and type label.
|
|
82
148
|
* @param {{ x: number, y: number, width: number, height: number, side?: 'left' | 'right' | 'top' | 'bottom', primaryConnectionPosition?: number, lineWidth?: number, color?: string, fill?: string, entries?: object[], typeLabel?: object }} connector
|
|
@@ -85,25 +151,19 @@ export class SchematicHarnessRenderer {
|
|
|
85
151
|
* @returns {string}
|
|
86
152
|
*/
|
|
87
153
|
static #buildConnectorMarkup(connector, sheetHeight, sheet) {
|
|
88
|
-
const stroke = SchematicColorResolver.resolveNonTextColor(
|
|
89
|
-
connector.color,
|
|
90
|
-
'--schematic-default-ink-color',
|
|
91
|
-
true
|
|
92
|
-
)
|
|
93
|
-
const fill = SchematicColorResolver.resolveFill(
|
|
94
|
-
connector.fill,
|
|
95
|
-
'--schematic-fill-light-color'
|
|
96
|
-
)
|
|
97
154
|
const textOptions =
|
|
98
155
|
SchematicTypography.buildDefaultSchematicFontOptions(sheet)
|
|
156
|
+
const geometry = SchematicHarnessRenderer.#connectorGeometry(
|
|
157
|
+
connector,
|
|
158
|
+
sheetHeight
|
|
159
|
+
)
|
|
99
160
|
const entryMarkup = (connector.entries || [])
|
|
100
161
|
.map((entry) =>
|
|
101
162
|
SchematicHarnessRenderer.#buildEntryMarkup(
|
|
102
163
|
connector,
|
|
103
164
|
entry,
|
|
104
165
|
sheetHeight,
|
|
105
|
-
textOptions
|
|
106
|
-
stroke
|
|
166
|
+
textOptions
|
|
107
167
|
)
|
|
108
168
|
)
|
|
109
169
|
.join('')
|
|
@@ -117,20 +177,14 @@ export class SchematicHarnessRenderer {
|
|
|
117
177
|
|
|
118
178
|
return (
|
|
119
179
|
'<g class="schematic-harness-connector">' +
|
|
120
|
-
'<
|
|
121
|
-
escapeHtml(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
) +
|
|
127
|
-
'" fill="' +
|
|
128
|
-
escapeHtml(fill) +
|
|
129
|
-
'" stroke="' +
|
|
130
|
-
escapeHtml(stroke) +
|
|
131
|
-
'" stroke-width="' +
|
|
180
|
+
'<path class="schematic-harness-connector__body" d="' +
|
|
181
|
+
escapeHtml(geometry.bodyPath) +
|
|
182
|
+
'" fill="var(--schematic-pin-marker-fill)" stroke="none" />' +
|
|
183
|
+
'<path class="schematic-harness-connector__bracket" d="' +
|
|
184
|
+
escapeHtml(geometry.bracketPath) +
|
|
185
|
+
'" fill="none" stroke="var(--schematic-accent-ink-color)" stroke-opacity="0.46" stroke-width="' +
|
|
132
186
|
formatNumber(Math.max(Number(connector.lineWidth) || 1, 1)) +
|
|
133
|
-
'" stroke-
|
|
187
|
+
'" stroke-linecap="round" />' +
|
|
134
188
|
entryMarkup +
|
|
135
189
|
typeMarkup +
|
|
136
190
|
'</g>'
|
|
@@ -138,68 +192,204 @@ export class SchematicHarnessRenderer {
|
|
|
138
192
|
}
|
|
139
193
|
|
|
140
194
|
/**
|
|
141
|
-
* Builds the
|
|
195
|
+
* Builds the filled connector region and open primary-side bracket.
|
|
142
196
|
* @param {{ x: number, y: number, width: number, height: number, side?: 'left' | 'right' | 'top' | 'bottom', primaryConnectionPosition?: number }} connector
|
|
143
197
|
* @param {number} sheetHeight
|
|
144
|
-
* @returns {string}
|
|
198
|
+
* @returns {{ bodyPath: string, bracketPath: string }}
|
|
145
199
|
*/
|
|
146
|
-
static #
|
|
200
|
+
static #connectorGeometry(connector, sheetHeight) {
|
|
147
201
|
const x = Number(connector.x) || 0
|
|
148
|
-
const y = Number(connector.y) || 0
|
|
149
202
|
const width = Math.max(Number(connector.width) || 0, 1)
|
|
150
203
|
const height = Math.max(Number(connector.height) || 0, 1)
|
|
151
204
|
const inset = Math.min(12, width / 3, height / 3)
|
|
205
|
+
const top = projectSchematicY(sheetHeight, Number(connector.y) || 0)
|
|
206
|
+
const bottom = top + height
|
|
207
|
+
const left = x
|
|
208
|
+
const right = x + width
|
|
209
|
+
const side = connector.side || 'left'
|
|
210
|
+
const primaryExtent =
|
|
211
|
+
side === 'top' || side === 'bottom' ? width : height
|
|
152
212
|
const primary = Math.min(
|
|
153
213
|
Math.max(Number(connector.primaryConnectionPosition) || 0, 0),
|
|
154
|
-
|
|
155
|
-
? width
|
|
156
|
-
: height
|
|
214
|
+
primaryExtent
|
|
157
215
|
)
|
|
158
|
-
const
|
|
159
|
-
|
|
216
|
+
const primaryX = left + primary
|
|
217
|
+
const primaryY = top + primary
|
|
218
|
+
const xCurve = inset * 0.7
|
|
219
|
+
const yUpperCurve = Math.max((primaryY - top) * 0.45, 1)
|
|
220
|
+
const yLowerCurve = Math.max((bottom - primaryY) * 0.45, 1)
|
|
221
|
+
const xLeftCurve = Math.max((primaryX - left) * 0.45, 1)
|
|
222
|
+
const xRightCurve = Math.max((right - primaryX) * 0.45, 1)
|
|
160
223
|
|
|
161
224
|
if (side === 'right') {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
225
|
+
const inner = right - inset
|
|
226
|
+
const forward =
|
|
227
|
+
'M ' +
|
|
228
|
+
formatNumber(inner) +
|
|
229
|
+
' ' +
|
|
230
|
+
formatNumber(top) +
|
|
231
|
+
' C ' +
|
|
232
|
+
formatNumber(inner + xCurve) +
|
|
233
|
+
' ' +
|
|
234
|
+
formatNumber(top) +
|
|
235
|
+
' ' +
|
|
236
|
+
formatNumber(right) +
|
|
237
|
+
' ' +
|
|
238
|
+
formatNumber(primaryY - yUpperCurve) +
|
|
239
|
+
' ' +
|
|
240
|
+
formatNumber(right) +
|
|
241
|
+
' ' +
|
|
242
|
+
formatNumber(primaryY) +
|
|
243
|
+
' C ' +
|
|
244
|
+
formatNumber(right) +
|
|
245
|
+
' ' +
|
|
246
|
+
formatNumber(primaryY + yLowerCurve) +
|
|
247
|
+
' ' +
|
|
248
|
+
formatNumber(inner + xCurve) +
|
|
249
|
+
' ' +
|
|
250
|
+
formatNumber(bottom) +
|
|
251
|
+
' ' +
|
|
252
|
+
formatNumber(inner) +
|
|
253
|
+
' ' +
|
|
254
|
+
formatNumber(bottom)
|
|
255
|
+
return {
|
|
256
|
+
bracketPath: forward,
|
|
257
|
+
bodyPath:
|
|
258
|
+
forward +
|
|
259
|
+
' H ' +
|
|
260
|
+
formatNumber(left) +
|
|
261
|
+
' V ' +
|
|
262
|
+
formatNumber(top) +
|
|
263
|
+
' Z'
|
|
264
|
+
}
|
|
193
265
|
}
|
|
194
266
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
if (side === 'top') {
|
|
268
|
+
const inner = top + inset
|
|
269
|
+
const forward =
|
|
270
|
+
'M ' +
|
|
271
|
+
formatNumber(left) +
|
|
272
|
+
' ' +
|
|
273
|
+
formatNumber(inner) +
|
|
274
|
+
' C ' +
|
|
275
|
+
formatNumber(left) +
|
|
276
|
+
' ' +
|
|
277
|
+
formatNumber(inner - xCurve) +
|
|
278
|
+
' ' +
|
|
279
|
+
formatNumber(primaryX - xLeftCurve) +
|
|
280
|
+
' ' +
|
|
281
|
+
formatNumber(top) +
|
|
282
|
+
' ' +
|
|
283
|
+
formatNumber(primaryX) +
|
|
284
|
+
' ' +
|
|
285
|
+
formatNumber(top) +
|
|
286
|
+
' C ' +
|
|
287
|
+
formatNumber(primaryX + xRightCurve) +
|
|
288
|
+
' ' +
|
|
289
|
+
formatNumber(top) +
|
|
290
|
+
' ' +
|
|
291
|
+
formatNumber(right) +
|
|
292
|
+
' ' +
|
|
293
|
+
formatNumber(inner - xCurve) +
|
|
294
|
+
' ' +
|
|
295
|
+
formatNumber(right) +
|
|
296
|
+
' ' +
|
|
297
|
+
formatNumber(inner)
|
|
298
|
+
return {
|
|
299
|
+
bracketPath: forward,
|
|
300
|
+
bodyPath:
|
|
301
|
+
forward +
|
|
302
|
+
' V ' +
|
|
303
|
+
formatNumber(bottom) +
|
|
304
|
+
' H ' +
|
|
305
|
+
formatNumber(left) +
|
|
306
|
+
' Z'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (side === 'bottom') {
|
|
311
|
+
const inner = bottom - inset
|
|
312
|
+
const forward =
|
|
313
|
+
'M ' +
|
|
314
|
+
formatNumber(left) +
|
|
315
|
+
' ' +
|
|
316
|
+
formatNumber(inner) +
|
|
317
|
+
' C ' +
|
|
318
|
+
formatNumber(left) +
|
|
319
|
+
' ' +
|
|
320
|
+
formatNumber(inner + xCurve) +
|
|
321
|
+
' ' +
|
|
322
|
+
formatNumber(primaryX - xLeftCurve) +
|
|
323
|
+
' ' +
|
|
324
|
+
formatNumber(bottom) +
|
|
325
|
+
' ' +
|
|
326
|
+
formatNumber(primaryX) +
|
|
327
|
+
' ' +
|
|
328
|
+
formatNumber(bottom) +
|
|
329
|
+
' C ' +
|
|
330
|
+
formatNumber(primaryX + xRightCurve) +
|
|
331
|
+
' ' +
|
|
332
|
+
formatNumber(bottom) +
|
|
333
|
+
' ' +
|
|
334
|
+
formatNumber(right) +
|
|
335
|
+
' ' +
|
|
336
|
+
formatNumber(inner + xCurve) +
|
|
337
|
+
' ' +
|
|
338
|
+
formatNumber(right) +
|
|
339
|
+
' ' +
|
|
340
|
+
formatNumber(inner)
|
|
341
|
+
return {
|
|
342
|
+
bracketPath: forward,
|
|
343
|
+
bodyPath:
|
|
344
|
+
forward +
|
|
345
|
+
' V ' +
|
|
346
|
+
formatNumber(top) +
|
|
347
|
+
' H ' +
|
|
348
|
+
formatNumber(left) +
|
|
349
|
+
' Z'
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const inner = left + inset
|
|
354
|
+
const forward =
|
|
355
|
+
'M ' +
|
|
356
|
+
formatNumber(inner) +
|
|
357
|
+
' ' +
|
|
358
|
+
formatNumber(top) +
|
|
359
|
+
' C ' +
|
|
360
|
+
formatNumber(inner - xCurve) +
|
|
361
|
+
' ' +
|
|
362
|
+
formatNumber(top) +
|
|
363
|
+
' ' +
|
|
364
|
+
formatNumber(left) +
|
|
365
|
+
' ' +
|
|
366
|
+
formatNumber(primaryY - yUpperCurve) +
|
|
367
|
+
' ' +
|
|
368
|
+
formatNumber(left) +
|
|
369
|
+
' ' +
|
|
370
|
+
formatNumber(primaryY) +
|
|
371
|
+
' C ' +
|
|
372
|
+
formatNumber(left) +
|
|
373
|
+
' ' +
|
|
374
|
+
formatNumber(primaryY + yLowerCurve) +
|
|
375
|
+
' ' +
|
|
376
|
+
formatNumber(inner - xCurve) +
|
|
377
|
+
' ' +
|
|
378
|
+
formatNumber(bottom) +
|
|
379
|
+
' ' +
|
|
380
|
+
formatNumber(inner) +
|
|
381
|
+
' ' +
|
|
382
|
+
formatNumber(bottom)
|
|
383
|
+
return {
|
|
384
|
+
bracketPath: forward,
|
|
385
|
+
bodyPath:
|
|
386
|
+
forward +
|
|
387
|
+
' H ' +
|
|
388
|
+
formatNumber(right) +
|
|
389
|
+
' V ' +
|
|
390
|
+
formatNumber(top) +
|
|
391
|
+
' Z'
|
|
392
|
+
}
|
|
203
393
|
}
|
|
204
394
|
|
|
205
395
|
/**
|
|
@@ -208,45 +398,27 @@ export class SchematicHarnessRenderer {
|
|
|
208
398
|
* @param {{ name?: string, side?: 'left' | 'right' | 'top' | 'bottom', distanceFromTop?: number, textColor?: string }} entry
|
|
209
399
|
* @param {number} sheetHeight
|
|
210
400
|
* @param {{ fontSize: number, fontFamily: string, fontWeight: number }} textOptions
|
|
211
|
-
* @param {string} connectorStroke
|
|
212
401
|
* @returns {string}
|
|
213
402
|
*/
|
|
214
|
-
static #buildEntryMarkup(
|
|
215
|
-
connector,
|
|
216
|
-
entry,
|
|
217
|
-
sheetHeight,
|
|
218
|
-
textOptions,
|
|
219
|
-
connectorStroke
|
|
220
|
-
) {
|
|
403
|
+
static #buildEntryMarkup(connector, entry, sheetHeight, textOptions) {
|
|
221
404
|
const placement = SchematicHarnessRenderer.#entryPlacement(
|
|
222
405
|
connector,
|
|
223
406
|
entry,
|
|
224
407
|
sheetHeight
|
|
225
408
|
)
|
|
226
|
-
const labelColor = SchematicColorResolver.resolveColor(
|
|
227
|
-
entry.textColor,
|
|
228
|
-
'--schematic-default-ink-color',
|
|
229
|
-
true
|
|
230
|
-
)
|
|
231
409
|
|
|
232
410
|
return (
|
|
233
|
-
'<g class="schematic-harness-entry"><
|
|
234
|
-
formatNumber(placement.
|
|
235
|
-
'"
|
|
236
|
-
formatNumber(placement.
|
|
237
|
-
'"
|
|
238
|
-
formatNumber(placement.x2) +
|
|
239
|
-
'" y2="' +
|
|
240
|
-
formatNumber(placement.y2) +
|
|
241
|
-
'" stroke="' +
|
|
242
|
-
escapeHtml(connectorStroke) +
|
|
243
|
-
'" />' +
|
|
411
|
+
'<g class="schematic-harness-entry"><circle class="schematic-harness-entry-dot" cx="' +
|
|
412
|
+
formatNumber(placement.dotX) +
|
|
413
|
+
'" cy="' +
|
|
414
|
+
formatNumber(placement.dotY) +
|
|
415
|
+
'" r="1.5" fill="var(--schematic-default-ink-color)" />' +
|
|
244
416
|
createSvgText(
|
|
245
417
|
'schematic-harness-entry-label',
|
|
246
418
|
placement.labelX,
|
|
247
419
|
placement.labelY,
|
|
248
420
|
entry.name || '',
|
|
249
|
-
|
|
421
|
+
'var(--schematic-default-ink-color)',
|
|
250
422
|
placement.anchor,
|
|
251
423
|
textOptions
|
|
252
424
|
) +
|
|
@@ -259,7 +431,7 @@ export class SchematicHarnessRenderer {
|
|
|
259
431
|
* @param {{ x: number, y: number, width: number, height: number }} connector
|
|
260
432
|
* @param {{ side?: 'left' | 'right' | 'top' | 'bottom', distanceFromTop?: number }} entry
|
|
261
433
|
* @param {number} sheetHeight
|
|
262
|
-
* @returns {{
|
|
434
|
+
* @returns {{ dotX: number, dotY: number, labelX: number, labelY: number, anchor: 'start' | 'middle' | 'end' }}
|
|
263
435
|
*/
|
|
264
436
|
static #entryPlacement(connector, entry, sheetHeight) {
|
|
265
437
|
const x = Number(connector.x) || 0
|
|
@@ -273,25 +445,21 @@ export class SchematicHarnessRenderer {
|
|
|
273
445
|
if (side === 'left') {
|
|
274
446
|
const entryY = projectSchematicY(sheetHeight, y - distance)
|
|
275
447
|
return {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
y2: entryY,
|
|
280
|
-
labelX: x - 14,
|
|
448
|
+
dotX: x,
|
|
449
|
+
dotY: entryY,
|
|
450
|
+
labelX: x + 8,
|
|
281
451
|
labelY: entryY + baselineLift,
|
|
282
|
-
anchor: '
|
|
452
|
+
anchor: 'start'
|
|
283
453
|
}
|
|
284
454
|
}
|
|
285
455
|
if (side === 'top') {
|
|
286
456
|
const entryX = x + distance
|
|
287
457
|
const entryY = projectSchematicY(sheetHeight, y)
|
|
288
458
|
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
x2: entryX,
|
|
292
|
-
y2: entryY - 10,
|
|
459
|
+
dotX: entryX,
|
|
460
|
+
dotY: entryY,
|
|
293
461
|
labelX: entryX,
|
|
294
|
-
labelY: entryY
|
|
462
|
+
labelY: entryY + 12,
|
|
295
463
|
anchor: 'middle'
|
|
296
464
|
}
|
|
297
465
|
}
|
|
@@ -299,25 +467,21 @@ export class SchematicHarnessRenderer {
|
|
|
299
467
|
const entryX = x + distance
|
|
300
468
|
const entryY = projectSchematicY(sheetHeight, y - height)
|
|
301
469
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
x2: entryX,
|
|
305
|
-
y2: entryY + 10,
|
|
470
|
+
dotX: entryX,
|
|
471
|
+
dotY: entryY,
|
|
306
472
|
labelX: entryX,
|
|
307
|
-
labelY: entryY
|
|
473
|
+
labelY: entryY - 5,
|
|
308
474
|
anchor: 'middle'
|
|
309
475
|
}
|
|
310
476
|
}
|
|
311
477
|
|
|
312
478
|
const entryY = projectSchematicY(sheetHeight, y - distance)
|
|
313
479
|
return {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
y2: entryY,
|
|
318
|
-
labelX: x + width + 14,
|
|
480
|
+
dotX: x + width,
|
|
481
|
+
dotY: entryY,
|
|
482
|
+
labelX: x + width - 8,
|
|
319
483
|
labelY: entryY + baselineLift,
|
|
320
|
-
anchor: '
|
|
484
|
+
anchor: 'end'
|
|
321
485
|
}
|
|
322
486
|
}
|
|
323
487
|
|
|
@@ -334,11 +498,7 @@ export class SchematicHarnessRenderer {
|
|
|
334
498
|
typeLabel.x,
|
|
335
499
|
projectSchematicY(sheetHeight, typeLabel.y),
|
|
336
500
|
typeLabel.text || '',
|
|
337
|
-
|
|
338
|
-
typeLabel.color,
|
|
339
|
-
'--schematic-default-ink-color',
|
|
340
|
-
true
|
|
341
|
-
),
|
|
501
|
+
'var(--schematic-default-ink-color)',
|
|
342
502
|
'start',
|
|
343
503
|
textOptions
|
|
344
504
|
)
|