altium-toolkit 1.1.2 → 1.1.22
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/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
|
@@ -11,7 +11,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
11
11
|
* Resolves one native-facing pin text placement in renderer coordinates
|
|
12
12
|
* before sheet Y projection. The returned `yOffset` is applied after
|
|
13
13
|
* projection, matching SVG text baseline behavior.
|
|
14
|
-
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number }} pin
|
|
14
|
+
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number, electrical?: number }} pin
|
|
15
15
|
* @param {'name' | 'number'} labelKind
|
|
16
16
|
* @param {{ labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', rotateTopNumber?: boolean }} [options]
|
|
17
17
|
* @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
|
|
@@ -19,7 +19,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
19
19
|
static resolveNativePinTextPlacement(pin, labelKind, options = {}) {
|
|
20
20
|
const labelMode = options.labelMode || 'name-and-number'
|
|
21
21
|
const markerStyle =
|
|
22
|
-
SchematicOwnerPinLabelLayout.#
|
|
22
|
+
SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
|
|
23
23
|
|
|
24
24
|
if (labelKind === 'number') {
|
|
25
25
|
return SchematicOwnerPinLabelLayout.#resolveNumberPlacement(
|
|
@@ -143,6 +143,124 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
143
143
|
return offsets
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Collects compact FET-like owner groups whose numeric contact labels need
|
|
148
|
+
* to stay outside the owner-drawn device body.
|
|
149
|
+
* @param {{ ownerIndex?: string, name?: string, designator?: string, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
150
|
+
* @returns {Map<string, 'left' | 'right'>}
|
|
151
|
+
*/
|
|
152
|
+
static collectCompactExternalNumberLabelSides(pins) {
|
|
153
|
+
const ownerPins = new Map()
|
|
154
|
+
|
|
155
|
+
for (const pin of pins) {
|
|
156
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
157
|
+
if (!ownerIndex) continue
|
|
158
|
+
if (!ownerPins.has(ownerIndex)) ownerPins.set(ownerIndex, [])
|
|
159
|
+
ownerPins.get(ownerIndex).push(pin)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sides = new Map()
|
|
163
|
+
|
|
164
|
+
for (const [ownerIndex, groupedPins] of ownerPins.entries()) {
|
|
165
|
+
const side =
|
|
166
|
+
SchematicOwnerPinLabelLayout.#resolveCompactExternalNumberLabelSide(
|
|
167
|
+
groupedPins
|
|
168
|
+
)
|
|
169
|
+
if (side) sides.set(ownerIndex, side)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return sides
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Collects rectangular owner bodies whose numeric-only horizontal pin
|
|
177
|
+
* labels are drawn inside the body, close to the body edge.
|
|
178
|
+
* @param {{ ownerIndex?: string, name?: string, designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
179
|
+
* @param {{ ownerIndex?: string, x: number, y: number, width: number, height: number }[]} rectangles
|
|
180
|
+
* @returns {Map<string, { left: number, right: number }>}
|
|
181
|
+
*/
|
|
182
|
+
static collectInternalNumberLabelBoxes(pins, rectangles) {
|
|
183
|
+
const ownerPins = SchematicOwnerPinLabelLayout.#groupByOwnerIndex(pins)
|
|
184
|
+
const ownerRectangles =
|
|
185
|
+
SchematicOwnerPinLabelLayout.#groupByOwnerIndex(rectangles)
|
|
186
|
+
const boxes = new Map()
|
|
187
|
+
|
|
188
|
+
for (const [ownerIndex, groupedPins] of ownerPins.entries()) {
|
|
189
|
+
const box =
|
|
190
|
+
SchematicOwnerPinLabelLayout.#resolveInternalNumberLabelBox(
|
|
191
|
+
groupedPins,
|
|
192
|
+
ownerRectangles.get(ownerIndex) || []
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if (box) {
|
|
196
|
+
boxes.set(ownerIndex, box)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return boxes
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Collects internally numbered pins whose external number would overlap a
|
|
205
|
+
* visible route text label on the same horizontal lane.
|
|
206
|
+
* @param {{ ownerIndex?: string, name?: string, designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', electrical?: number, symbolOuter?: number }[]} pins
|
|
207
|
+
* @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number }[]} texts
|
|
208
|
+
* @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
|
|
209
|
+
* @returns {Set<string>}
|
|
210
|
+
*/
|
|
211
|
+
static collectOverlappingExternalNumberLabelKeys(
|
|
212
|
+
pins,
|
|
213
|
+
texts,
|
|
214
|
+
internalNumberLabelBoxes
|
|
215
|
+
) {
|
|
216
|
+
const textBounds = (texts || [])
|
|
217
|
+
.map((text) =>
|
|
218
|
+
SchematicOwnerPinLabelLayout.#estimateHorizontalTextBounds(text)
|
|
219
|
+
)
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
const keys = new Set()
|
|
222
|
+
|
|
223
|
+
if (textBounds.length === 0) {
|
|
224
|
+
return keys
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const pin of pins || []) {
|
|
228
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
229
|
+
if (!ownerIndex || !internalNumberLabelBoxes.has(ownerIndex)) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!SchematicOwnerPinLabelLayout.#isInternalNumberLabelPin(pin)) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const numberBounds =
|
|
238
|
+
SchematicOwnerPinLabelLayout.#estimateExternalNumberBounds(pin)
|
|
239
|
+
if (!numberBounds) {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
textBounds.some((bounds) =>
|
|
245
|
+
SchematicOwnerPinLabelLayout.#boundsOverlap(
|
|
246
|
+
numberBounds,
|
|
247
|
+
bounds,
|
|
248
|
+
1.5
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
) {
|
|
252
|
+
keys.add(
|
|
253
|
+
SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
|
|
254
|
+
ownerIndex,
|
|
255
|
+
pin.designator
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return keys
|
|
262
|
+
}
|
|
263
|
+
|
|
146
264
|
/**
|
|
147
265
|
* Resolves the final SVG text anchor for one schematic free-text label.
|
|
148
266
|
* Mirrored rotated owner pin-name labels need the opposite text edge so
|
|
@@ -170,6 +288,361 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
170
288
|
return Number(text.y) >= Number(matchedOwnerPin.y) ? 'end' : 'start'
|
|
171
289
|
}
|
|
172
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Resolves the side that should carry compact owner-drawn pin numbers.
|
|
293
|
+
* @param {{ name?: string, designator?: string, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
294
|
+
* @returns {'left' | 'right' | null}
|
|
295
|
+
*/
|
|
296
|
+
static #resolveCompactExternalNumberLabelSide(pins) {
|
|
297
|
+
if (
|
|
298
|
+
pins.length !== 4 ||
|
|
299
|
+
!pins.every(
|
|
300
|
+
(pin) =>
|
|
301
|
+
(pin.labelMode || 'name-and-number') === 'number-only' &&
|
|
302
|
+
/^\d+$/.test(String(pin.designator || '').trim()) &&
|
|
303
|
+
Math.abs(Number(pin.length || 0)) <= 20 &&
|
|
304
|
+
SchematicOwnerPinLabelLayout.#isFetTerminalName(pin.name)
|
|
305
|
+
)
|
|
306
|
+
) {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const hasTopPin = pins.some((pin) => pin.orientation === 'top')
|
|
311
|
+
const hasBottomPin = pins.some((pin) => pin.orientation === 'bottom')
|
|
312
|
+
const horizontalPins = pins.filter(
|
|
313
|
+
(pin) => pin.orientation === 'left' || pin.orientation === 'right'
|
|
314
|
+
)
|
|
315
|
+
const horizontalSides = new Set(
|
|
316
|
+
horizontalPins.map((pin) => pin.orientation)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
!hasTopPin ||
|
|
321
|
+
!hasBottomPin ||
|
|
322
|
+
horizontalPins.length === 0 ||
|
|
323
|
+
horizontalSides.size !== 1
|
|
324
|
+
) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return horizontalPins[0].orientation
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resolves one rectangular body suitable for internal numeric pin labels.
|
|
333
|
+
* @param {{ name?: string, designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
334
|
+
* @param {{ x: number, y: number, width: number, height: number }[]} rectangles
|
|
335
|
+
* @returns {{ left: number, right: number } | null}
|
|
336
|
+
*/
|
|
337
|
+
static #resolveInternalNumberLabelBox(pins, rectangles) {
|
|
338
|
+
if (
|
|
339
|
+
pins.length < 4 ||
|
|
340
|
+
!pins.every((pin) =>
|
|
341
|
+
SchematicOwnerPinLabelLayout.#isInternalNumberLabelPin(pin)
|
|
342
|
+
)
|
|
343
|
+
) {
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sides = new Set(pins.map((pin) => pin.orientation))
|
|
348
|
+
|
|
349
|
+
if (!sides.has('left') || !sides.has('right') || sides.size !== 2) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const rectangle =
|
|
354
|
+
SchematicOwnerPinLabelLayout.#findInternalNumberLabelRectangle(
|
|
355
|
+
pins,
|
|
356
|
+
rectangles
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if (!rectangle) {
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
left: rectangle.left,
|
|
365
|
+
right: rectangle.right
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Returns the owner rectangle whose vertical edges carry every pin.
|
|
371
|
+
* @param {{ x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
372
|
+
* @param {{ x: number, y: number, width: number, height: number }[]} rectangles
|
|
373
|
+
* @returns {{ left: number, right: number, top: number, bottom: number } | null}
|
|
374
|
+
*/
|
|
375
|
+
static #findInternalNumberLabelRectangle(pins, rectangles) {
|
|
376
|
+
for (const rectangle of rectangles) {
|
|
377
|
+
const normalized =
|
|
378
|
+
SchematicOwnerPinLabelLayout.#normalizeRectangle(rectangle)
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
normalized &&
|
|
382
|
+
SchematicOwnerPinLabelLayout.#pinsAlignWithRectangleEdges(
|
|
383
|
+
pins,
|
|
384
|
+
normalized
|
|
385
|
+
)
|
|
386
|
+
) {
|
|
387
|
+
return normalized
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Returns true when every pin body endpoint lies on a vertical body edge.
|
|
396
|
+
* @param {{ x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
397
|
+
* @param {{ left: number, right: number, top: number, bottom: number }} rectangle
|
|
398
|
+
* @returns {boolean}
|
|
399
|
+
*/
|
|
400
|
+
static #pinsAlignWithRectangleEdges(pins, rectangle) {
|
|
401
|
+
const tolerance = 1.5
|
|
402
|
+
|
|
403
|
+
for (const pin of pins) {
|
|
404
|
+
const x = Number(pin.x)
|
|
405
|
+
const y = Number(pin.y)
|
|
406
|
+
|
|
407
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
y < rectangle.top - tolerance ||
|
|
413
|
+
y > rectangle.bottom + tolerance
|
|
414
|
+
) {
|
|
415
|
+
return false
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (
|
|
419
|
+
pin.orientation === 'left' &&
|
|
420
|
+
Math.abs(x - rectangle.left) > tolerance
|
|
421
|
+
) {
|
|
422
|
+
return false
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
pin.orientation === 'right' &&
|
|
427
|
+
Math.abs(x - rectangle.right) > tolerance
|
|
428
|
+
) {
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return true
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Normalizes a rectangle to absolute edge coordinates.
|
|
438
|
+
* @param {{ x: number, y: number, width: number, height: number }} rectangle
|
|
439
|
+
* @returns {{ left: number, right: number, top: number, bottom: number } | null}
|
|
440
|
+
*/
|
|
441
|
+
static #normalizeRectangle(rectangle) {
|
|
442
|
+
const x1 = Number(rectangle?.x)
|
|
443
|
+
const y1 = Number(rectangle?.y)
|
|
444
|
+
const x2 = x1 + Number(rectangle?.width)
|
|
445
|
+
const y2 = y1 + Number(rectangle?.height)
|
|
446
|
+
|
|
447
|
+
if (
|
|
448
|
+
!Number.isFinite(x1) ||
|
|
449
|
+
!Number.isFinite(y1) ||
|
|
450
|
+
!Number.isFinite(x2) ||
|
|
451
|
+
!Number.isFinite(y2) ||
|
|
452
|
+
x1 === x2 ||
|
|
453
|
+
y1 === y2
|
|
454
|
+
) {
|
|
455
|
+
return null
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
left: Math.min(x1, x2),
|
|
460
|
+
right: Math.max(x1, x2),
|
|
461
|
+
top: Math.min(y1, y2),
|
|
462
|
+
bottom: Math.max(y1, y2)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Returns true for numeric-only horizontal pins on short owner stubs.
|
|
468
|
+
* @param {{ name?: string, designator?: string, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }} pin
|
|
469
|
+
* @returns {boolean}
|
|
470
|
+
*/
|
|
471
|
+
static #isInternalNumberLabelPin(pin) {
|
|
472
|
+
const name = String(pin.name || '').trim()
|
|
473
|
+
const designator = String(pin.designator || '').trim()
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
(pin.orientation === 'left' || pin.orientation === 'right') &&
|
|
477
|
+
(pin.labelMode || 'name-and-number') === 'number-only' &&
|
|
478
|
+
/^\d+$/.test(designator) &&
|
|
479
|
+
(!name || /^\d+$/.test(name)) &&
|
|
480
|
+
Math.abs(Number(pin.length || 0)) <= 30
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Groups schematic owner-local primitives by owner index.
|
|
486
|
+
* @template T
|
|
487
|
+
* @param {(T & { ownerIndex?: string })[]} items
|
|
488
|
+
* @returns {Map<string, T[]>}
|
|
489
|
+
*/
|
|
490
|
+
static #groupByOwnerIndex(items) {
|
|
491
|
+
const groups = new Map()
|
|
492
|
+
|
|
493
|
+
for (const item of items) {
|
|
494
|
+
const ownerIndex = String(item.ownerIndex || '').trim()
|
|
495
|
+
|
|
496
|
+
if (!ownerIndex) {
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!groups.has(ownerIndex)) {
|
|
501
|
+
groups.set(ownerIndex, [])
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
groups.get(ownerIndex).push(item)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return groups
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Returns true for FET terminal names, including numbered gate/source pins.
|
|
512
|
+
* @param {string | undefined} name
|
|
513
|
+
* @returns {boolean}
|
|
514
|
+
*/
|
|
515
|
+
static #isFetTerminalName(name) {
|
|
516
|
+
return /^(?:[DS]|[GS]\d*)$/i.test(String(name || '').trim())
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Estimates a route text label's source-coordinate visual bounds.
|
|
521
|
+
* @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number } | null} text
|
|
522
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
|
|
523
|
+
*/
|
|
524
|
+
static #estimateHorizontalTextBounds(text) {
|
|
525
|
+
if (!text || text.hidden || text.ownerIndex) return null
|
|
526
|
+
if (Number(text.rotation || 0) !== 0) return null
|
|
527
|
+
|
|
528
|
+
const label = String(text.resolvedText ?? text.text ?? '').trim()
|
|
529
|
+
const x = Number(text.x)
|
|
530
|
+
const y = Number(text.y)
|
|
531
|
+
if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
532
|
+
|
|
533
|
+
const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize(
|
|
534
|
+
text.fontSize
|
|
535
|
+
)
|
|
536
|
+
const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
|
|
537
|
+
label,
|
|
538
|
+
fontSize
|
|
539
|
+
)
|
|
540
|
+
const anchor = text.anchor || 'start'
|
|
541
|
+
const minX =
|
|
542
|
+
anchor === 'end'
|
|
543
|
+
? x - width
|
|
544
|
+
: anchor === 'middle'
|
|
545
|
+
? x - width / 2
|
|
546
|
+
: x
|
|
547
|
+
const maxX =
|
|
548
|
+
anchor === 'end'
|
|
549
|
+
? x
|
|
550
|
+
: anchor === 'middle'
|
|
551
|
+
? x + width / 2
|
|
552
|
+
: x + width
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
minX,
|
|
556
|
+
maxX,
|
|
557
|
+
minY: y - fontSize * 0.7,
|
|
558
|
+
maxY: y + fontSize * 0.35
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Estimates one external pin number's source-coordinate bounds.
|
|
564
|
+
* @param {{ designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number }} pin
|
|
565
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
|
|
566
|
+
*/
|
|
567
|
+
static #estimateExternalNumberBounds(pin) {
|
|
568
|
+
if (pin.orientation !== 'left' && pin.orientation !== 'right') {
|
|
569
|
+
return null
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const label = String(pin.designator || '').trim()
|
|
573
|
+
const x = Number(pin.x)
|
|
574
|
+
const y = Number(pin.y)
|
|
575
|
+
if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
576
|
+
|
|
577
|
+
const markerStyle =
|
|
578
|
+
SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
|
|
579
|
+
const clearance =
|
|
580
|
+
SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberClearance(
|
|
581
|
+
markerStyle,
|
|
582
|
+
pin
|
|
583
|
+
)
|
|
584
|
+
const offset = Math.max(
|
|
585
|
+
clearance,
|
|
586
|
+
SchematicOwnerPinLabelLayout.#resolveCompactExternalHorizontalNumberOffset(
|
|
587
|
+
pin
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
const numberX = pin.orientation === 'left' ? x - offset : x + offset
|
|
591
|
+
const anchor =
|
|
592
|
+
SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
593
|
+
pin,
|
|
594
|
+
markerStyle
|
|
595
|
+
)
|
|
596
|
+
const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize()
|
|
597
|
+
const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
|
|
598
|
+
label,
|
|
599
|
+
fontSize
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
minX: anchor === 'end' ? numberX - width : numberX,
|
|
604
|
+
maxX: anchor === 'end' ? numberX : numberX + width,
|
|
605
|
+
minY: y - fontSize * 0.7,
|
|
606
|
+
maxY: y + fontSize * 0.35
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Returns the viewer-adjusted schematic font size used for collision
|
|
612
|
+
* estimates.
|
|
613
|
+
* @param {number | undefined} fontSize Source font size.
|
|
614
|
+
* @returns {number}
|
|
615
|
+
*/
|
|
616
|
+
static #resolveViewerFontSize(fontSize = 10) {
|
|
617
|
+
return Math.max(Number(fontSize || 10) - 1, 6)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Estimates one rendered text run width.
|
|
622
|
+
* @param {string} text Text content.
|
|
623
|
+
* @param {number} fontSize Viewer font size.
|
|
624
|
+
* @returns {number}
|
|
625
|
+
*/
|
|
626
|
+
static #estimateTextWidth(text, fontSize) {
|
|
627
|
+
return Math.max(String(text || '').length * fontSize * 0.62, fontSize)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Returns true when two source-coordinate boxes overlap.
|
|
632
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} first First bounds.
|
|
633
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} second Second bounds.
|
|
634
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
635
|
+
* @returns {boolean}
|
|
636
|
+
*/
|
|
637
|
+
static #boundsOverlap(first, second, tolerance) {
|
|
638
|
+
return (
|
|
639
|
+
first.minX <= second.maxX + tolerance &&
|
|
640
|
+
first.maxX >= second.minX - tolerance &&
|
|
641
|
+
first.minY <= second.maxY + tolerance &&
|
|
642
|
+
first.maxY >= second.minY - tolerance
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
173
646
|
/**
|
|
174
647
|
* Moves left/right pin numbers outward by the same horizontal correction
|
|
175
648
|
* already applied to their explicit owner pin-name labels.
|
|
@@ -206,7 +679,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
206
679
|
/**
|
|
207
680
|
* Resolves schematic pin-number placement.
|
|
208
681
|
* @param {{ x: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
209
|
-
* @param {'single-in' | 'single-out' | 'double' | null} markerStyle
|
|
682
|
+
* @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
|
|
210
683
|
* @param {{ rotateTopNumber?: boolean }} options
|
|
211
684
|
* @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
|
|
212
685
|
*/
|
|
@@ -221,7 +694,10 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
221
694
|
pin
|
|
222
695
|
),
|
|
223
696
|
yOffset: -1,
|
|
224
|
-
anchor:
|
|
697
|
+
anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
698
|
+
pin,
|
|
699
|
+
markerStyle
|
|
700
|
+
),
|
|
225
701
|
rotation: 0
|
|
226
702
|
}
|
|
227
703
|
case 'right':
|
|
@@ -233,7 +709,10 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
233
709
|
pin
|
|
234
710
|
),
|
|
235
711
|
yOffset: -1,
|
|
236
|
-
anchor:
|
|
712
|
+
anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
713
|
+
pin,
|
|
714
|
+
markerStyle
|
|
715
|
+
),
|
|
237
716
|
rotation: 0
|
|
238
717
|
}
|
|
239
718
|
case 'top':
|
|
@@ -308,22 +787,46 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
308
787
|
|
|
309
788
|
/**
|
|
310
789
|
* Returns the horizontal pin-number clearance needed by the pin geometry.
|
|
311
|
-
* @param {'single-in' | 'single-out' | 'double' | null} markerStyle
|
|
312
|
-
* @param {{ length?: number }} pin
|
|
790
|
+
* @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
|
|
791
|
+
* @param {{ length?: number, electrical?: number }} pin
|
|
313
792
|
* @returns {number}
|
|
314
793
|
*/
|
|
315
794
|
static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
|
|
316
795
|
switch (markerStyle) {
|
|
317
796
|
case 'double':
|
|
318
|
-
return
|
|
797
|
+
return 21
|
|
798
|
+
case 'cross':
|
|
799
|
+
return 9
|
|
319
800
|
case 'single-in':
|
|
320
801
|
case 'single-out':
|
|
321
802
|
return 8
|
|
322
803
|
default:
|
|
804
|
+
if (Number(pin?.electrical || 0) === 1) {
|
|
805
|
+
return 16
|
|
806
|
+
}
|
|
807
|
+
|
|
323
808
|
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 2)
|
|
324
809
|
}
|
|
325
810
|
}
|
|
326
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Resolves the text edge used for horizontal pin numbers.
|
|
814
|
+
* @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin Pin primitive.
|
|
815
|
+
* @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle Marker style.
|
|
816
|
+
* @returns {'start' | 'end'}
|
|
817
|
+
*/
|
|
818
|
+
static #resolveHorizontalPinNumberAnchor(pin, markerStyle) {
|
|
819
|
+
const routeFacing =
|
|
820
|
+
markerStyle === 'double' ||
|
|
821
|
+
(!markerStyle && Number(pin?.electrical || 0) === 1)
|
|
822
|
+
|
|
823
|
+
if (routeFacing) {
|
|
824
|
+
return pin.orientation === 'left' ? 'start' : 'end'
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return pin.orientation === 'left' ? 'end' : 'start'
|
|
828
|
+
}
|
|
829
|
+
|
|
327
830
|
/**
|
|
328
831
|
* Returns the horizontal pin-name inset used inside the symbol body.
|
|
329
832
|
* @param {{ length?: number }} pin
|
|
@@ -335,7 +838,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
335
838
|
return 10
|
|
336
839
|
}
|
|
337
840
|
|
|
338
|
-
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin,
|
|
841
|
+
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 7)
|
|
339
842
|
}
|
|
340
843
|
|
|
341
844
|
/**
|
|
@@ -354,10 +857,22 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
354
857
|
return fallback === 2 ? 10 : 8
|
|
355
858
|
}
|
|
356
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Resolves the stub-side offset used when a pin already has an internal
|
|
862
|
+
* numeric label in its owner body.
|
|
863
|
+
* @param {{ length?: number }} pin Pin primitive.
|
|
864
|
+
* @returns {number}
|
|
865
|
+
*/
|
|
866
|
+
static #resolveCompactExternalHorizontalNumberOffset(pin) {
|
|
867
|
+
const length = Math.abs(Number(pin.length || 0))
|
|
868
|
+
|
|
869
|
+
return Math.max(8, Math.min(length - 6, 12))
|
|
870
|
+
}
|
|
871
|
+
|
|
357
872
|
/**
|
|
358
873
|
* Resolves one authored outer pin marker style from the stored symbol flag.
|
|
359
874
|
* @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
360
|
-
* @returns {'single-in' | 'single-out' | 'double' | null}
|
|
875
|
+
* @returns {'single-in' | 'single-out' | 'double' | 'cross' | null}
|
|
361
876
|
*/
|
|
362
877
|
static #resolveOuterPinMarkerStyle(pin) {
|
|
363
878
|
if (pin.orientation !== 'left' && pin.orientation !== 'right') {
|
|
@@ -370,10 +885,45 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
370
885
|
return 'single-out'
|
|
371
886
|
case 2:
|
|
372
887
|
return 'single-in'
|
|
888
|
+
case 6:
|
|
889
|
+
return 'cross'
|
|
373
890
|
case 34:
|
|
374
891
|
return 'double'
|
|
375
892
|
default:
|
|
376
893
|
return null
|
|
377
894
|
}
|
|
378
895
|
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Resolves the marker style that contributes to native text clearance.
|
|
899
|
+
* @param {{ symbolOuter?: number, electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
900
|
+
* @returns {'single-in' | 'single-out' | 'double' | 'cross' | null}
|
|
901
|
+
*/
|
|
902
|
+
static #resolvePinMarkerStyle(pin) {
|
|
903
|
+
return (
|
|
904
|
+
SchematicOwnerPinLabelLayout.#resolveOuterPinMarkerStyle(pin) ||
|
|
905
|
+
SchematicOwnerPinLabelLayout.#resolveElectricalPinMarkerStyle(pin)
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Resolves electrical pin marker styles. Bidirectional pins keep the
|
|
911
|
+
* existing route-facing number placement through the explicit check.
|
|
912
|
+
* @param {{ electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
913
|
+
* @returns {'single-in' | 'single-out' | null}
|
|
914
|
+
*/
|
|
915
|
+
static #resolveElectricalPinMarkerStyle(pin) {
|
|
916
|
+
if (pin.orientation !== 'left' && pin.orientation !== 'right') {
|
|
917
|
+
return null
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
switch (Number(pin.electrical)) {
|
|
921
|
+
case 0:
|
|
922
|
+
return 'single-in'
|
|
923
|
+
case 2:
|
|
924
|
+
return 'single-out'
|
|
925
|
+
default:
|
|
926
|
+
return null
|
|
927
|
+
}
|
|
928
|
+
}
|
|
379
929
|
}
|