altium-toolkit 1.4.9 → 1.4.11
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/docs/release-notes-v1.4.10.md +29 -0
- package/docs/release-notes-v1.4.11.md +34 -0
- package/package.json +1 -1
- package/src/convergence/AltiumSchematicFidelityNormalizer.mjs +661 -0
- package/src/convergence/AltiumSchematicNativeFooterOwnerAligner.mjs +351 -0
- package/src/convergence/SchematicSvgRenderer.mjs +37 -2
- package/src/ui/SchematicHarnessRenderer.mjs +346 -0
- package/src/ui/SchematicRotatedOwnerTextPlacement.mjs +55 -0
|
@@ -0,0 +1,346 @@
|
|
|
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 { SchematicTypography } from './SchematicTypography.mjs'
|
|
7
|
+
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
8
|
+
|
|
9
|
+
const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
|
|
10
|
+
SchematicSvgUtils
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders normalized signal harness trunks, connectors, entries, and labels.
|
|
14
|
+
*/
|
|
15
|
+
export class SchematicHarnessRenderer {
|
|
16
|
+
/**
|
|
17
|
+
* Builds complete harness markup.
|
|
18
|
+
* @param {{ connectors?: object[], signalHarnesses?: object[] } | null | undefined} harnesses
|
|
19
|
+
* @param {number} sheetHeight
|
|
20
|
+
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static buildMarkup(harnesses, sheetHeight, sheet) {
|
|
24
|
+
const signalHarnessMarkup = (harnesses?.signalHarnesses || [])
|
|
25
|
+
.map((signalHarness) =>
|
|
26
|
+
SchematicHarnessRenderer.#buildSignalHarnessMarkup(
|
|
27
|
+
signalHarness,
|
|
28
|
+
sheetHeight
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
.join('')
|
|
32
|
+
const connectorMarkup = (harnesses?.connectors || [])
|
|
33
|
+
.map((connector) =>
|
|
34
|
+
SchematicHarnessRenderer.#buildConnectorMarkup(
|
|
35
|
+
connector,
|
|
36
|
+
sheetHeight,
|
|
37
|
+
sheet
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
.join('')
|
|
41
|
+
|
|
42
|
+
return signalHarnessMarkup + connectorMarkup
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds one signal-harness polyline.
|
|
47
|
+
* @param {{ points?: { x: number, y: number }[], color?: string, lineWidth?: number }} signalHarness
|
|
48
|
+
* @param {number} sheetHeight
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
static #buildSignalHarnessMarkup(signalHarness, sheetHeight) {
|
|
52
|
+
const points = (signalHarness.points || [])
|
|
53
|
+
.map(
|
|
54
|
+
(point) =>
|
|
55
|
+
formatNumber(point.x) +
|
|
56
|
+
',' +
|
|
57
|
+
formatNumber(projectSchematicY(sheetHeight, point.y))
|
|
58
|
+
)
|
|
59
|
+
.join(' ')
|
|
60
|
+
|
|
61
|
+
if (!points) return ''
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
'<polyline class="schematic-signal-harness" points="' +
|
|
65
|
+
escapeHtml(points) +
|
|
66
|
+
'" fill="none" stroke="' +
|
|
67
|
+
escapeHtml(
|
|
68
|
+
SchematicColorResolver.resolveNonTextColor(
|
|
69
|
+
signalHarness.color,
|
|
70
|
+
'--schematic-default-ink-color',
|
|
71
|
+
true
|
|
72
|
+
)
|
|
73
|
+
) +
|
|
74
|
+
'" stroke-width="' +
|
|
75
|
+
formatNumber(Math.max(Number(signalHarness.lineWidth) || 1, 1)) +
|
|
76
|
+
'" stroke-linecap="round" stroke-linejoin="round" />'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds one harness connector with its entry labels and type label.
|
|
82
|
+
* @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
|
|
83
|
+
* @param {number} sheetHeight
|
|
84
|
+
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
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
|
+
const textOptions =
|
|
98
|
+
SchematicTypography.buildDefaultSchematicFontOptions(sheet)
|
|
99
|
+
const entryMarkup = (connector.entries || [])
|
|
100
|
+
.map((entry) =>
|
|
101
|
+
SchematicHarnessRenderer.#buildEntryMarkup(
|
|
102
|
+
connector,
|
|
103
|
+
entry,
|
|
104
|
+
sheetHeight,
|
|
105
|
+
textOptions,
|
|
106
|
+
stroke
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
.join('')
|
|
110
|
+
const typeMarkup = connector.typeLabel
|
|
111
|
+
? SchematicHarnessRenderer.#buildTypeLabelMarkup(
|
|
112
|
+
connector.typeLabel,
|
|
113
|
+
sheetHeight,
|
|
114
|
+
textOptions
|
|
115
|
+
)
|
|
116
|
+
: ''
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
'<g class="schematic-harness-connector">' +
|
|
120
|
+
'<polygon points="' +
|
|
121
|
+
escapeHtml(
|
|
122
|
+
SchematicHarnessRenderer.#connectorPoints(
|
|
123
|
+
connector,
|
|
124
|
+
sheetHeight
|
|
125
|
+
)
|
|
126
|
+
) +
|
|
127
|
+
'" fill="' +
|
|
128
|
+
escapeHtml(fill) +
|
|
129
|
+
'" stroke="' +
|
|
130
|
+
escapeHtml(stroke) +
|
|
131
|
+
'" stroke-width="' +
|
|
132
|
+
formatNumber(Math.max(Number(connector.lineWidth) || 1, 1)) +
|
|
133
|
+
'" stroke-linejoin="round" />' +
|
|
134
|
+
entryMarkup +
|
|
135
|
+
typeMarkup +
|
|
136
|
+
'</g>'
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds the concave connector outline from its primary connection side.
|
|
142
|
+
* @param {{ x: number, y: number, width: number, height: number, side?: 'left' | 'right' | 'top' | 'bottom', primaryConnectionPosition?: number }} connector
|
|
143
|
+
* @param {number} sheetHeight
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
static #connectorPoints(connector, sheetHeight) {
|
|
147
|
+
const x = Number(connector.x) || 0
|
|
148
|
+
const y = Number(connector.y) || 0
|
|
149
|
+
const width = Math.max(Number(connector.width) || 0, 1)
|
|
150
|
+
const height = Math.max(Number(connector.height) || 0, 1)
|
|
151
|
+
const inset = Math.min(12, width / 3, height / 3)
|
|
152
|
+
const primary = Math.min(
|
|
153
|
+
Math.max(Number(connector.primaryConnectionPosition) || 0, 0),
|
|
154
|
+
connector.side === 'top' || connector.side === 'bottom'
|
|
155
|
+
? width
|
|
156
|
+
: height
|
|
157
|
+
)
|
|
158
|
+
const side = connector.side || 'left'
|
|
159
|
+
let points
|
|
160
|
+
|
|
161
|
+
if (side === 'right') {
|
|
162
|
+
points = [
|
|
163
|
+
{ x, y },
|
|
164
|
+
{ x: x + width - inset, y },
|
|
165
|
+
{ x: x + width, y: y - primary },
|
|
166
|
+
{ x: x + width - inset, y: y - height },
|
|
167
|
+
{ x, y: y - height }
|
|
168
|
+
]
|
|
169
|
+
} else if (side === 'top') {
|
|
170
|
+
points = [
|
|
171
|
+
{ x, y: y - inset },
|
|
172
|
+
{ x: x + primary, y },
|
|
173
|
+
{ x: x + width, y: y - inset },
|
|
174
|
+
{ x: x + width, y: y - height },
|
|
175
|
+
{ x, y: y - height }
|
|
176
|
+
]
|
|
177
|
+
} else if (side === 'bottom') {
|
|
178
|
+
points = [
|
|
179
|
+
{ x, y },
|
|
180
|
+
{ x: x + width, y },
|
|
181
|
+
{ x: x + width, y: y - height + inset },
|
|
182
|
+
{ x: x + primary, y: y - height },
|
|
183
|
+
{ x, y: y - height + inset }
|
|
184
|
+
]
|
|
185
|
+
} else {
|
|
186
|
+
points = [
|
|
187
|
+
{ x: x + inset, y },
|
|
188
|
+
{ x: x + width, y },
|
|
189
|
+
{ x: x + width, y: y - height },
|
|
190
|
+
{ x: x + inset, y: y - height },
|
|
191
|
+
{ x, y: y - primary }
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return points
|
|
196
|
+
.map(
|
|
197
|
+
(point) =>
|
|
198
|
+
formatNumber(point.x) +
|
|
199
|
+
',' +
|
|
200
|
+
formatNumber(projectSchematicY(sheetHeight, point.y))
|
|
201
|
+
)
|
|
202
|
+
.join(' ')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds one harness-entry stub and label.
|
|
207
|
+
* @param {{ x: number, y: number, width: number, height: number }} connector
|
|
208
|
+
* @param {{ name?: string, side?: 'left' | 'right' | 'top' | 'bottom', distanceFromTop?: number, textColor?: string }} entry
|
|
209
|
+
* @param {number} sheetHeight
|
|
210
|
+
* @param {{ fontSize: number, fontFamily: string, fontWeight: number }} textOptions
|
|
211
|
+
* @param {string} connectorStroke
|
|
212
|
+
* @returns {string}
|
|
213
|
+
*/
|
|
214
|
+
static #buildEntryMarkup(
|
|
215
|
+
connector,
|
|
216
|
+
entry,
|
|
217
|
+
sheetHeight,
|
|
218
|
+
textOptions,
|
|
219
|
+
connectorStroke
|
|
220
|
+
) {
|
|
221
|
+
const placement = SchematicHarnessRenderer.#entryPlacement(
|
|
222
|
+
connector,
|
|
223
|
+
entry,
|
|
224
|
+
sheetHeight
|
|
225
|
+
)
|
|
226
|
+
const labelColor = SchematicColorResolver.resolveColor(
|
|
227
|
+
entry.textColor,
|
|
228
|
+
'--schematic-default-ink-color',
|
|
229
|
+
true
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
'<g class="schematic-harness-entry"><line x1="' +
|
|
234
|
+
formatNumber(placement.x1) +
|
|
235
|
+
'" y1="' +
|
|
236
|
+
formatNumber(placement.y1) +
|
|
237
|
+
'" x2="' +
|
|
238
|
+
formatNumber(placement.x2) +
|
|
239
|
+
'" y2="' +
|
|
240
|
+
formatNumber(placement.y2) +
|
|
241
|
+
'" stroke="' +
|
|
242
|
+
escapeHtml(connectorStroke) +
|
|
243
|
+
'" />' +
|
|
244
|
+
createSvgText(
|
|
245
|
+
'schematic-harness-entry-label',
|
|
246
|
+
placement.labelX,
|
|
247
|
+
placement.labelY,
|
|
248
|
+
entry.name || '',
|
|
249
|
+
labelColor,
|
|
250
|
+
placement.anchor,
|
|
251
|
+
textOptions
|
|
252
|
+
) +
|
|
253
|
+
'</g>'
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolves the entry stub and text placement for every connector side.
|
|
259
|
+
* @param {{ x: number, y: number, width: number, height: number }} connector
|
|
260
|
+
* @param {{ side?: 'left' | 'right' | 'top' | 'bottom', distanceFromTop?: number }} entry
|
|
261
|
+
* @param {number} sheetHeight
|
|
262
|
+
* @returns {{ x1: number, y1: number, x2: number, y2: number, labelX: number, labelY: number, anchor: 'start' | 'middle' | 'end' }}
|
|
263
|
+
*/
|
|
264
|
+
static #entryPlacement(connector, entry, sheetHeight) {
|
|
265
|
+
const x = Number(connector.x) || 0
|
|
266
|
+
const y = Number(connector.y) || 0
|
|
267
|
+
const width = Number(connector.width) || 0
|
|
268
|
+
const height = Number(connector.height) || 0
|
|
269
|
+
const distance = Number(entry.distanceFromTop) || 0
|
|
270
|
+
const side = entry.side || 'right'
|
|
271
|
+
const baselineLift = 3
|
|
272
|
+
|
|
273
|
+
if (side === 'left') {
|
|
274
|
+
const entryY = projectSchematicY(sheetHeight, y - distance)
|
|
275
|
+
return {
|
|
276
|
+
x1: x,
|
|
277
|
+
y1: entryY,
|
|
278
|
+
x2: x - 10,
|
|
279
|
+
y2: entryY,
|
|
280
|
+
labelX: x - 14,
|
|
281
|
+
labelY: entryY + baselineLift,
|
|
282
|
+
anchor: 'end'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (side === 'top') {
|
|
286
|
+
const entryX = x + distance
|
|
287
|
+
const entryY = projectSchematicY(sheetHeight, y)
|
|
288
|
+
return {
|
|
289
|
+
x1: entryX,
|
|
290
|
+
y1: entryY,
|
|
291
|
+
x2: entryX,
|
|
292
|
+
y2: entryY - 10,
|
|
293
|
+
labelX: entryX,
|
|
294
|
+
labelY: entryY - 13,
|
|
295
|
+
anchor: 'middle'
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (side === 'bottom') {
|
|
299
|
+
const entryX = x + distance
|
|
300
|
+
const entryY = projectSchematicY(sheetHeight, y - height)
|
|
301
|
+
return {
|
|
302
|
+
x1: entryX,
|
|
303
|
+
y1: entryY,
|
|
304
|
+
x2: entryX,
|
|
305
|
+
y2: entryY + 10,
|
|
306
|
+
labelX: entryX,
|
|
307
|
+
labelY: entryY + 19,
|
|
308
|
+
anchor: 'middle'
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const entryY = projectSchematicY(sheetHeight, y - distance)
|
|
313
|
+
return {
|
|
314
|
+
x1: x + width,
|
|
315
|
+
y1: entryY,
|
|
316
|
+
x2: x + width + 10,
|
|
317
|
+
y2: entryY,
|
|
318
|
+
labelX: x + width + 14,
|
|
319
|
+
labelY: entryY + baselineLift,
|
|
320
|
+
anchor: 'start'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Builds the connector harness-type label.
|
|
326
|
+
* @param {{ text?: string, x: number, y: number, color?: string }} typeLabel
|
|
327
|
+
* @param {number} sheetHeight
|
|
328
|
+
* @param {{ fontSize: number, fontFamily: string, fontWeight: number }} textOptions
|
|
329
|
+
* @returns {string}
|
|
330
|
+
*/
|
|
331
|
+
static #buildTypeLabelMarkup(typeLabel, sheetHeight, textOptions) {
|
|
332
|
+
return createSvgText(
|
|
333
|
+
'schematic-harness-type',
|
|
334
|
+
typeLabel.x,
|
|
335
|
+
projectSchematicY(sheetHeight, typeLabel.y),
|
|
336
|
+
typeLabel.text || '',
|
|
337
|
+
SchematicColorResolver.resolveColor(
|
|
338
|
+
typeLabel.color,
|
|
339
|
+
'--schematic-default-ink-color',
|
|
340
|
+
true
|
|
341
|
+
),
|
|
342
|
+
'start',
|
|
343
|
+
textOptions
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Places vertical owner annotations in columns clear of narrow symbol bodies.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicRotatedOwnerTextPlacement {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the horizontal baseline for one rotated owner annotation.
|
|
11
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text
|
|
12
|
+
* @param {number} sourceX
|
|
13
|
+
* @param {number} fontSize
|
|
14
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBodyBounds
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
static resolveX(text, sourceX, fontSize, ownerBodyBounds) {
|
|
18
|
+
const numericX = Number(sourceX)
|
|
19
|
+
const numericFontSize = Number(fontSize)
|
|
20
|
+
const ownerIndex = String(text?.ownerIndex || '').trim()
|
|
21
|
+
const rotation = SchematicRotatedOwnerTextPlacement.#normalizeDegrees(
|
|
22
|
+
text?.rotation
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
text?.recordType !== '41' ||
|
|
27
|
+
!ownerIndex ||
|
|
28
|
+
(rotation !== 90 && rotation !== 270) ||
|
|
29
|
+
!Number.isFinite(numericX) ||
|
|
30
|
+
!Number.isFinite(numericFontSize) ||
|
|
31
|
+
numericFontSize <= 0
|
|
32
|
+
) {
|
|
33
|
+
return numericX
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const bounds = ownerBodyBounds.get(ownerIndex)
|
|
37
|
+
if (!bounds || numericX < Number(bounds.maxX)) {
|
|
38
|
+
return numericX
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return numericX + numericFontSize
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes a source angle to the positive 0-359 degree range.
|
|
46
|
+
* @param {number | undefined} value
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
static #normalizeDegrees(value) {
|
|
50
|
+
const numericValue = Number(value)
|
|
51
|
+
if (!Number.isFinite(numericValue)) return 0
|
|
52
|
+
|
|
53
|
+
return ((numericValue % 360) + 360) % 360
|
|
54
|
+
}
|
|
55
|
+
}
|