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.
@@ -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
+ }