altium-toolkit 0.1.20 → 0.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/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +15 -3
- package/src/core/altium/PcbFontMetricsParser.mjs +33 -6
- package/src/core/altium/PcbModelParser.mjs +82 -0
- package/src/core/altium/PcbTextPrimitiveParser.mjs +48 -3
- package/src/core/altium/SchematicPinDesignatorInferer.mjs +401 -0
- package/src/core/altium/SchematicPinParser.mjs +136 -56
- package/src/core/altium/SchematicStreamExtractor.mjs +93 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +40 -12
- package/src/scene3d.mjs +1 -0
- package/src/ui/PcbScene3dBuilder.mjs +574 -145
- package/src/ui/PcbScene3dDrillCutoutBuilder.mjs +453 -0
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +272 -0
- package/src/ui/PcbScene3dTextBoxLayoutResolver.mjs +95 -0
- package/src/ui/SchematicImageRenderer.mjs +134 -32
- package/src/ui/SchematicJunctionRenderer.mjs +22 -4
- package/src/ui/SchematicNoteRenderer.mjs +73 -9
- package/src/ui/SchematicPinSvgRenderer.mjs +25 -3
- package/src/ui/SchematicPowerPortRenderer.mjs +183 -36
- package/src/ui/SchematicSvgRenderer.mjs +2 -2
|
@@ -3,21 +3,32 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs'
|
|
6
|
+
import { PcbScene3dDrillCutoutBuilder } from './PcbScene3dDrillCutoutBuilder.mjs'
|
|
6
7
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
7
8
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
9
|
+
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
10
|
+
import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Builds deterministic 3D scene data from the normalized PCB model.
|
|
11
14
|
*/
|
|
12
15
|
export class PcbScene3dBuilder {
|
|
16
|
+
static #DENSE_OVERLAY_FILL_COLOR = 0xf8f6ef
|
|
17
|
+
static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
|
|
18
|
+
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
19
|
+
static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
|
|
20
|
+
static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 5
|
|
21
|
+
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* Builds a scene description for host 3D renderers.
|
|
15
25
|
* @param {{ pcb?: { boardOutline?: { widthMil?: number, heightMil?: number, minX?: number, minY?: number, segments?: Array<Record<string, number | string>> }, primitiveLayers?: { layerId: number, name: string }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[], tracks?: any[], arcs?: any[], fills?: any[], vias?: any[], polygons?: any[], embeddedModels?: any[], componentBodies?: { modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }[], components?: { designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }[] } }} documentModel
|
|
16
26
|
* @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
|
|
17
|
-
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[] }, bottom: { fills: any[], tracks: any[], arcs: any[] } } } }}
|
|
27
|
+
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
|
|
18
28
|
*/
|
|
19
29
|
static build(documentModel, options = {}) {
|
|
20
30
|
const pcb = documentModel?.pcb || {}
|
|
31
|
+
const appearance3d = pcb.appearance3d || {}
|
|
21
32
|
const boardOutline = pcb.boardOutline || {}
|
|
22
33
|
const primitiveLayers = Array.isArray(pcb.primitiveLayers)
|
|
23
34
|
? pcb.primitiveLayers
|
|
@@ -30,6 +41,10 @@ export class PcbScene3dBuilder {
|
|
|
30
41
|
const tracks = Array.isArray(pcb.tracks) ? pcb.tracks : []
|
|
31
42
|
const arcs = Array.isArray(pcb.arcs) ? pcb.arcs : []
|
|
32
43
|
const fills = Array.isArray(pcb.fills) ? pcb.fills : []
|
|
44
|
+
const texts = Array.isArray(pcb.texts) ? pcb.texts : []
|
|
45
|
+
const vias = Array.isArray(pcb.vias) ? pcb.vias : []
|
|
46
|
+
const silkscreenRegions =
|
|
47
|
+
PcbScene3dBuilder.#resolveSilkscreenRegions(pcb)
|
|
33
48
|
const thicknessMil = Number(options.boardThicknessMil || 63) || 63
|
|
34
49
|
const modelRegistry = options.modelRegistry || null
|
|
35
50
|
const board = {
|
|
@@ -52,8 +67,40 @@ export class PcbScene3dBuilder {
|
|
|
52
67
|
componentBodies,
|
|
53
68
|
components
|
|
54
69
|
)
|
|
70
|
+
const topSilkscreen = PcbScene3dBuilder.#buildSilkscreenSide(
|
|
71
|
+
primitiveLayers,
|
|
72
|
+
fills,
|
|
73
|
+
tracks,
|
|
74
|
+
arcs,
|
|
75
|
+
texts,
|
|
76
|
+
silkscreenRegions,
|
|
77
|
+
boardOutline,
|
|
78
|
+
'top',
|
|
79
|
+
pads,
|
|
80
|
+
vias
|
|
81
|
+
)
|
|
82
|
+
const bottomSilkscreen = PcbScene3dBuilder.#buildSilkscreenSide(
|
|
83
|
+
primitiveLayers,
|
|
84
|
+
fills,
|
|
85
|
+
tracks,
|
|
86
|
+
arcs,
|
|
87
|
+
texts,
|
|
88
|
+
silkscreenRegions,
|
|
89
|
+
boardOutline,
|
|
90
|
+
'bottom',
|
|
91
|
+
pads,
|
|
92
|
+
vias
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
PcbScene3dBuilder.#applySilkscreenAppearance(
|
|
96
|
+
topSilkscreen,
|
|
97
|
+
bottomSilkscreen,
|
|
98
|
+
board,
|
|
99
|
+
appearance3d
|
|
100
|
+
)
|
|
55
101
|
|
|
56
102
|
return {
|
|
103
|
+
sourceFormat: 'altium',
|
|
57
104
|
board,
|
|
58
105
|
components: components.map((component) =>
|
|
59
106
|
PcbScene3dBuilder.#buildComponent(
|
|
@@ -69,6 +116,7 @@ export class PcbScene3dBuilder {
|
|
|
69
116
|
PcbScene3dBuilder.#buildExternalPlacement(
|
|
70
117
|
componentBody,
|
|
71
118
|
bodyMatches[index],
|
|
119
|
+
components,
|
|
72
120
|
board,
|
|
73
121
|
thicknessMil,
|
|
74
122
|
modelRegistry
|
|
@@ -76,33 +124,18 @@ export class PcbScene3dBuilder {
|
|
|
76
124
|
)
|
|
77
125
|
.filter(Boolean),
|
|
78
126
|
detail: {
|
|
127
|
+
embeddedFonts: Array.isArray(pcb.embeddedFonts)
|
|
128
|
+
? pcb.embeddedFonts
|
|
129
|
+
: [],
|
|
79
130
|
pads,
|
|
80
131
|
tracks,
|
|
81
132
|
arcs,
|
|
82
133
|
fills,
|
|
83
|
-
vias
|
|
134
|
+
vias,
|
|
84
135
|
polygons: Array.isArray(pcb.polygons) ? pcb.polygons : [],
|
|
85
136
|
silkscreen: {
|
|
86
|
-
top:
|
|
87
|
-
|
|
88
|
-
primitiveLayers,
|
|
89
|
-
fills,
|
|
90
|
-
tracks,
|
|
91
|
-
arcs,
|
|
92
|
-
'top'
|
|
93
|
-
),
|
|
94
|
-
boardOutline
|
|
95
|
-
),
|
|
96
|
-
bottom: PcbEdgeFacingGlyphNormalizer.normalize(
|
|
97
|
-
PcbFootprintPrimitiveSelector.select(
|
|
98
|
-
primitiveLayers,
|
|
99
|
-
fills,
|
|
100
|
-
tracks,
|
|
101
|
-
arcs,
|
|
102
|
-
'bottom'
|
|
103
|
-
),
|
|
104
|
-
boardOutline
|
|
105
|
-
)
|
|
137
|
+
top: topSilkscreen,
|
|
138
|
+
bottom: bottomSilkscreen
|
|
106
139
|
}
|
|
107
140
|
}
|
|
108
141
|
}
|
|
@@ -163,8 +196,9 @@ export class PcbScene3dBuilder {
|
|
|
163
196
|
/**
|
|
164
197
|
* Builds one explicit external-model placement from normalized component
|
|
165
198
|
* body metadata.
|
|
166
|
-
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
|
|
199
|
+
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
|
|
167
200
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
|
|
201
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
|
|
168
202
|
* @param {{ centerX: number, centerY: number }} board
|
|
169
203
|
* @param {number} thicknessMil
|
|
170
204
|
* @param {{ resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null} modelRegistry
|
|
@@ -173,6 +207,7 @@ export class PcbScene3dBuilder {
|
|
|
173
207
|
static #buildExternalPlacement(
|
|
174
208
|
componentBody,
|
|
175
209
|
matchedComponent,
|
|
210
|
+
components,
|
|
176
211
|
board,
|
|
177
212
|
thicknessMil,
|
|
178
213
|
modelRegistry
|
|
@@ -190,16 +225,18 @@ export class PcbScene3dBuilder {
|
|
|
190
225
|
return null
|
|
191
226
|
}
|
|
192
227
|
|
|
193
|
-
const mountSide =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
const mountSide = PcbScene3dPlacementSideResolver.resolvePlacementSide(
|
|
229
|
+
componentBody,
|
|
230
|
+
matchedComponent,
|
|
231
|
+
components
|
|
232
|
+
)
|
|
197
233
|
const halfBoardThickness = thicknessMil / 2
|
|
198
234
|
const sourcePosition =
|
|
199
235
|
PcbScene3dBuilder.#resolveExternalPlacementSourcePosition(
|
|
200
|
-
componentBody
|
|
201
|
-
matchedComponent
|
|
236
|
+
componentBody
|
|
202
237
|
)
|
|
238
|
+
const modelRotation =
|
|
239
|
+
PcbScene3dBuilder.#resolveExternalModelRotation(componentBody)
|
|
203
240
|
|
|
204
241
|
return {
|
|
205
242
|
designator:
|
|
@@ -226,11 +263,7 @@ export class PcbScene3dBuilder {
|
|
|
226
263
|
},
|
|
227
264
|
bodyRotationDeg: Number(componentBody.rotationDeg || 0),
|
|
228
265
|
modelTransform: {
|
|
229
|
-
rotationDeg:
|
|
230
|
-
x: Number(componentBody.modelRotationDeg?.x || 0),
|
|
231
|
-
y: Number(componentBody.modelRotationDeg?.y || 0),
|
|
232
|
-
z: Number(componentBody.modelRotationDeg?.z || 0)
|
|
233
|
-
},
|
|
266
|
+
rotationDeg: modelRotation,
|
|
234
267
|
dzMil: Number(componentBody.dzMil || 0)
|
|
235
268
|
},
|
|
236
269
|
externalModel: resolvedModel
|
|
@@ -251,6 +284,10 @@ export class PcbScene3dBuilder {
|
|
|
251
284
|
const assignedBodyIndexes = new Set()
|
|
252
285
|
const assignedComponentIndexes = new Set()
|
|
253
286
|
const closeCandidates = []
|
|
287
|
+
const matchContext = PcbScene3dBuilder.#buildBodyMatchContext(
|
|
288
|
+
componentBodies,
|
|
289
|
+
components
|
|
290
|
+
)
|
|
254
291
|
|
|
255
292
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
256
293
|
components.forEach((component, componentIndex) => {
|
|
@@ -260,7 +297,15 @@ export class PcbScene3dBuilder {
|
|
|
260
297
|
component
|
|
261
298
|
)
|
|
262
299
|
|
|
263
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
distance <= 600 &&
|
|
302
|
+
PcbScene3dBuilder.#canUseCloseBodyComponentMatch(
|
|
303
|
+
componentBody,
|
|
304
|
+
component,
|
|
305
|
+
matchContext,
|
|
306
|
+
distance
|
|
307
|
+
)
|
|
308
|
+
) {
|
|
264
309
|
closeCandidates.push({
|
|
265
310
|
bodyIndex,
|
|
266
311
|
componentIndex,
|
|
@@ -288,7 +333,9 @@ export class PcbScene3dBuilder {
|
|
|
288
333
|
const groupedBodyIndexes = new Map()
|
|
289
334
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
290
335
|
const groupKey =
|
|
291
|
-
|
|
336
|
+
PcbScene3dPlacementSideResolver.resolveBodyGroupKey(
|
|
337
|
+
componentBody
|
|
338
|
+
)
|
|
292
339
|
if (!groupedBodyIndexes.has(groupKey)) {
|
|
293
340
|
groupedBodyIndexes.set(groupKey, [])
|
|
294
341
|
}
|
|
@@ -316,7 +363,7 @@ export class PcbScene3dBuilder {
|
|
|
316
363
|
bodyIndexes.includes(
|
|
317
364
|
matches.indexOf(components[componentIndex])
|
|
318
365
|
)) &&
|
|
319
|
-
|
|
366
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
320
367
|
referenceBody,
|
|
321
368
|
component
|
|
322
369
|
) > 0
|
|
@@ -349,6 +396,87 @@ export class PcbScene3dBuilder {
|
|
|
349
396
|
return matches
|
|
350
397
|
}
|
|
351
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Builds reusable identity statistics for body/component matching.
|
|
401
|
+
* @param {{ modelId?: string, name?: string, identifier?: string }[]} componentBodies
|
|
402
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string }[]} components
|
|
403
|
+
* @returns {{ bodyGroupCounts: Map<string, number>, candidateComponentCounts: Map<string, number> }}
|
|
404
|
+
*/
|
|
405
|
+
static #buildBodyMatchContext(componentBodies, components) {
|
|
406
|
+
const bodyGroupCounts = new Map()
|
|
407
|
+
const bodyByGroup = new Map()
|
|
408
|
+
const candidateComponentCounts = new Map()
|
|
409
|
+
|
|
410
|
+
for (const componentBody of componentBodies) {
|
|
411
|
+
const groupKey =
|
|
412
|
+
PcbScene3dPlacementSideResolver.resolveBodyGroupKey(
|
|
413
|
+
componentBody
|
|
414
|
+
)
|
|
415
|
+
bodyGroupCounts.set(
|
|
416
|
+
groupKey,
|
|
417
|
+
(bodyGroupCounts.get(groupKey) || 0) + 1
|
|
418
|
+
)
|
|
419
|
+
if (!bodyByGroup.has(groupKey)) {
|
|
420
|
+
bodyByGroup.set(groupKey, componentBody)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
bodyByGroup.forEach((componentBody, groupKey) => {
|
|
425
|
+
candidateComponentCounts.set(
|
|
426
|
+
groupKey,
|
|
427
|
+
components.filter(
|
|
428
|
+
(component) =>
|
|
429
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
430
|
+
componentBody,
|
|
431
|
+
component
|
|
432
|
+
) > 0
|
|
433
|
+
).length
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
return { bodyGroupCounts, candidateComponentCounts }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Returns true when a close body/component pair is identity-compatible and
|
|
442
|
+
* the body group can be matched one-to-one to component anchors.
|
|
443
|
+
* @param {{ modelId?: string, name?: string, identifier?: string }} componentBody
|
|
444
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string }} component
|
|
445
|
+
* @param {{ bodyGroupCounts: Map<string, number>, candidateComponentCounts: Map<string, number> }} matchContext
|
|
446
|
+
* @param {number} distanceMil Distance between body and component anchors.
|
|
447
|
+
* @returns {boolean}
|
|
448
|
+
*/
|
|
449
|
+
static #canUseCloseBodyComponentMatch(
|
|
450
|
+
componentBody,
|
|
451
|
+
component,
|
|
452
|
+
matchContext,
|
|
453
|
+
distanceMil
|
|
454
|
+
) {
|
|
455
|
+
if (
|
|
456
|
+
Number(distanceMil) <=
|
|
457
|
+
PcbScene3dBuilder.#PRECISE_BODY_MATCH_TOLERANCE_MIL
|
|
458
|
+
) {
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (
|
|
463
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
464
|
+
componentBody,
|
|
465
|
+
component
|
|
466
|
+
) <= 0
|
|
467
|
+
) {
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const groupKey =
|
|
472
|
+
PcbScene3dPlacementSideResolver.resolveBodyGroupKey(componentBody)
|
|
473
|
+
const bodyCount = matchContext.bodyGroupCounts.get(groupKey) || 0
|
|
474
|
+
const candidateCount =
|
|
475
|
+
matchContext.candidateComponentCounts.get(groupKey) || 0
|
|
476
|
+
|
|
477
|
+
return bodyCount > 0 && bodyCount <= candidateCount
|
|
478
|
+
}
|
|
479
|
+
|
|
352
480
|
/**
|
|
353
481
|
* Pairs one unresolved repeated body group with a repeated component group
|
|
354
482
|
* by preserving the dominant ordering axis and choosing the pairing that
|
|
@@ -506,23 +634,12 @@ export class PcbScene3dBuilder {
|
|
|
506
634
|
}
|
|
507
635
|
|
|
508
636
|
/**
|
|
509
|
-
* Returns the
|
|
637
|
+
* Returns the native body anchor that should be used for one explicit model
|
|
510
638
|
* placement.
|
|
511
639
|
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody
|
|
512
|
-
* @param {{ x: number, y: number } | null} matchedComponent
|
|
513
640
|
* @returns {{ x: number, y: number }}
|
|
514
641
|
*/
|
|
515
|
-
static #resolveExternalPlacementSourcePosition(
|
|
516
|
-
componentBody,
|
|
517
|
-
matchedComponent
|
|
518
|
-
) {
|
|
519
|
-
if (matchedComponent) {
|
|
520
|
-
return {
|
|
521
|
-
x: Number(matchedComponent.x || 0),
|
|
522
|
-
y: Number(matchedComponent.y || 0)
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
642
|
+
static #resolveExternalPlacementSourcePosition(componentBody) {
|
|
526
643
|
return {
|
|
527
644
|
x: Number(componentBody?.positionMil?.x || 0),
|
|
528
645
|
y: Number(componentBody?.positionMil?.y || 0)
|
|
@@ -530,20 +647,422 @@ export class PcbScene3dBuilder {
|
|
|
530
647
|
}
|
|
531
648
|
|
|
532
649
|
/**
|
|
533
|
-
* Resolves the authored placement rotation for one explicit external
|
|
534
|
-
*
|
|
535
|
-
* 2D model rotation offset carried by the body metadata.
|
|
650
|
+
* Resolves the authored placement rotation for one explicit external model.
|
|
651
|
+
* Altium stores the 3D model's board-facing yaw in MODEL.3D.ROTZ.
|
|
536
652
|
* @param {{ rotationDeg?: number }} componentBody
|
|
537
653
|
* @param {{ rotation?: number } | null} matchedComponent
|
|
538
654
|
* @returns {number}
|
|
539
655
|
*/
|
|
540
656
|
static #resolveExternalPlacementRotation(componentBody, matchedComponent) {
|
|
657
|
+
const modelRotationZ = Number(componentBody?.modelRotationDeg?.z)
|
|
658
|
+
if (Number.isFinite(modelRotationZ)) {
|
|
659
|
+
return PcbScene3dBuilder.#normalizeAngle(modelRotationZ)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return PcbScene3dBuilder.#normalizeAngle(
|
|
663
|
+
Number(componentBody?.rotationDeg || 0) +
|
|
664
|
+
Number(matchedComponent?.rotation || 0)
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Resolves model-local rotations after converting Altium's positive local
|
|
670
|
+
* rotation fields into the renderer's signed 3D model convention.
|
|
671
|
+
* @param {{ modelRotationDeg?: { x?: number, y?: number, z?: number } }} componentBody
|
|
672
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
673
|
+
*/
|
|
674
|
+
static #resolveExternalModelRotation(componentBody) {
|
|
675
|
+
return {
|
|
676
|
+
x: -Number(componentBody?.modelRotationDeg?.x || 0),
|
|
677
|
+
y: -Number(componentBody?.modelRotationDeg?.y || 0),
|
|
678
|
+
z: 0
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Selects and normalizes one board-side silkscreen primitive set.
|
|
684
|
+
* @param {{ layerId: number, name: string }[]} primitiveLayers
|
|
685
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[]} fills
|
|
686
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[]} tracks
|
|
687
|
+
* @param {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[]} arcs
|
|
688
|
+
* @param {{ text?: string, value?: string, x?: number, y?: number, height?: number, strokeWidth?: number, layerCode?: number, layerId?: number, visible?: boolean }[]} texts
|
|
689
|
+
* @param {{ points?: object[], holes?: object[][], layerCode?: number, layerId?: number }[]} regions
|
|
690
|
+
* @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number }} boardOutline
|
|
691
|
+
* @param {'top' | 'bottom'} side
|
|
692
|
+
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number, holeSlotLength?: number, slotLength?: number, rotation?: number, holeRotation?: number }[]} pads
|
|
693
|
+
* @param {{ x?: number, y?: number, holeDiameter?: number, drillDiameter?: number }[]} vias
|
|
694
|
+
* @returns {{ fills: object[], tracks: object[], arcs: object[], regions: object[], texts: object[], nativeTextKnockouts: boolean }}
|
|
695
|
+
*/
|
|
696
|
+
static #buildSilkscreenSide(
|
|
697
|
+
primitiveLayers,
|
|
698
|
+
fills,
|
|
699
|
+
tracks,
|
|
700
|
+
arcs,
|
|
701
|
+
texts,
|
|
702
|
+
regions,
|
|
703
|
+
boardOutline,
|
|
704
|
+
side,
|
|
705
|
+
pads,
|
|
706
|
+
vias
|
|
707
|
+
) {
|
|
708
|
+
const normalized = PcbEdgeFacingGlyphNormalizer.normalize(
|
|
709
|
+
PcbFootprintPrimitiveSelector.select(
|
|
710
|
+
primitiveLayers,
|
|
711
|
+
fills,
|
|
712
|
+
tracks,
|
|
713
|
+
arcs,
|
|
714
|
+
regions,
|
|
715
|
+
side
|
|
716
|
+
),
|
|
717
|
+
boardOutline
|
|
718
|
+
)
|
|
719
|
+
const fillsWithRegions = [
|
|
720
|
+
...(normalized.fills || []),
|
|
721
|
+
...(normalized.regions || [])
|
|
722
|
+
]
|
|
723
|
+
const denseOverlayArtwork = PcbScene3dBuilder.#isDenseOverlayArtwork(
|
|
724
|
+
{
|
|
725
|
+
fills: fillsWithRegions,
|
|
726
|
+
tracks: normalized.tracks,
|
|
727
|
+
arcs: normalized.arcs
|
|
728
|
+
},
|
|
729
|
+
boardOutline
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
...normalized,
|
|
734
|
+
denseOverlayArtwork,
|
|
735
|
+
nativeTextKnockouts: PcbScene3dBuilder.#hasNativeTextKnockouts(
|
|
736
|
+
fillsWithRegions,
|
|
737
|
+
normalized,
|
|
738
|
+
boardOutline
|
|
739
|
+
),
|
|
740
|
+
texts: PcbScene3dBuilder.#selectSilkscreenTexts(
|
|
741
|
+
primitiveLayers,
|
|
742
|
+
texts,
|
|
743
|
+
side
|
|
744
|
+
),
|
|
745
|
+
tracks: normalized.tracks,
|
|
746
|
+
fills: PcbScene3dDrillCutoutBuilder.clipFills(
|
|
747
|
+
fillsWithRegions,
|
|
748
|
+
pads,
|
|
749
|
+
vias
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Applies optional appearance hints for overlay artwork that carries broad
|
|
756
|
+
* silkscreen graphics plus dense board-colored linework.
|
|
757
|
+
* @param {{ fills?: any[], tracks?: any[], arcs?: any[], fillColor?: number, strokeColor?: number }} topSilkscreen
|
|
758
|
+
* @param {{ fills?: any[], tracks?: any[], arcs?: any[], fillColor?: number, strokeColor?: number }} bottomSilkscreen
|
|
759
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
760
|
+
* @param {{ silkscreenTopColor?: number, silkscreenBottomColor?: number }} appearance3d
|
|
761
|
+
* @returns {void}
|
|
762
|
+
*/
|
|
763
|
+
static #applySilkscreenAppearance(
|
|
764
|
+
topSilkscreen,
|
|
765
|
+
bottomSilkscreen,
|
|
766
|
+
board,
|
|
767
|
+
appearance3d
|
|
768
|
+
) {
|
|
769
|
+
PcbScene3dBuilder.#styleSilkscreenArtwork(
|
|
770
|
+
topSilkscreen,
|
|
771
|
+
board,
|
|
772
|
+
appearance3d.silkscreenTopColor
|
|
773
|
+
)
|
|
774
|
+
PcbScene3dBuilder.#styleSilkscreenArtwork(
|
|
775
|
+
bottomSilkscreen,
|
|
776
|
+
board,
|
|
777
|
+
appearance3d.silkscreenBottomColor
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Applies silkscreen colors and marks dense overlay art as light filled
|
|
783
|
+
* areas with app-board-colored strokes.
|
|
784
|
+
* @param {{ fills?: any[], tracks?: any[], arcs?: any[], fillColor?: number, strokeColor?: number, knockoutColor?: number, denseOverlayArtwork?: boolean }} side
|
|
785
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
786
|
+
* @param {number | undefined} silkscreenColor
|
|
787
|
+
* @returns {void}
|
|
788
|
+
*/
|
|
789
|
+
static #styleSilkscreenArtwork(side, board, silkscreenColor) {
|
|
790
|
+
if (Number.isInteger(silkscreenColor)) {
|
|
791
|
+
side.strokeColor = silkscreenColor
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (
|
|
795
|
+
!side?.denseOverlayArtwork &&
|
|
796
|
+
!PcbScene3dBuilder.#isDenseOverlayArtwork(side, board)
|
|
797
|
+
) {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
side.fillColor = Number.isInteger(silkscreenColor)
|
|
802
|
+
? silkscreenColor
|
|
803
|
+
: PcbScene3dBuilder.#DENSE_OVERLAY_FILL_COLOR
|
|
804
|
+
side.strokeColor = side.fillColor
|
|
805
|
+
side.knockoutColor = PcbScene3dBuilder.#DENSE_OVERLAY_KNOCKOUT_COLOR
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Selects visible side-specific silkscreen texts.
|
|
810
|
+
* @param {{ layerId: number, name: string }[]} primitiveLayers
|
|
811
|
+
* @param {{ text?: string, value?: string, x?: number, y?: number, height?: number, strokeWidth?: number, layerCode?: number, layerId?: number, visible?: boolean }[]} texts
|
|
812
|
+
* @param {'top' | 'bottom'} side
|
|
813
|
+
* @returns {object[]}
|
|
814
|
+
*/
|
|
815
|
+
static #selectSilkscreenTexts(primitiveLayers, texts, side) {
|
|
816
|
+
const layerIds = PcbScene3dBuilder.#resolveSilkscreenLayerIds(
|
|
817
|
+
primitiveLayers,
|
|
818
|
+
side
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
return (Array.isArray(texts) ? texts : [])
|
|
822
|
+
.filter((text) => text?.visible !== false)
|
|
823
|
+
.filter((text) => layerIds.has(Number(text?.layerId)))
|
|
824
|
+
.map((text) =>
|
|
825
|
+
PcbScene3dBuilder.#normalizeSilkscreenText(text, side)
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Resolves layer IDs that belong to one overlay side.
|
|
831
|
+
* @param {{ layerId: number, name: string }[]} primitiveLayers
|
|
832
|
+
* @param {'top' | 'bottom'} side
|
|
833
|
+
* @returns {Set<number>}
|
|
834
|
+
*/
|
|
835
|
+
static #resolveSilkscreenLayerIds(primitiveLayers, side) {
|
|
836
|
+
const needle = side === 'bottom' ? 'BOTTOM OVERLAY' : 'TOP OVERLAY'
|
|
837
|
+
|
|
838
|
+
return new Set(
|
|
839
|
+
(Array.isArray(primitiveLayers) ? primitiveLayers : [])
|
|
840
|
+
.filter((layer) =>
|
|
841
|
+
String(layer?.name || '')
|
|
842
|
+
.trim()
|
|
843
|
+
.toUpperCase()
|
|
844
|
+
.includes(needle)
|
|
845
|
+
)
|
|
846
|
+
.map((layer) => Number(layer.layerId))
|
|
847
|
+
.filter((layerId) => Number.isInteger(layerId))
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Normalizes one Altium overlay text into the runtime stroke-text shape.
|
|
853
|
+
* @param {{ text?: string, value?: string, x?: number, y?: number, height?: number, strokeWidth?: number, rotation?: number, mirrored?: boolean | number | string, isMirrored?: boolean | number | string, mirrorFlag?: boolean | number | string, Mirrored?: boolean | number | string, IsMirrored?: boolean | number | string, MirrorFlag?: boolean | number | string, layerId?: number }} text
|
|
854
|
+
* @param {'top' | 'bottom'} side
|
|
855
|
+
* @returns {object}
|
|
856
|
+
*/
|
|
857
|
+
static #normalizeSilkscreenText(text, side) {
|
|
858
|
+
const height = Math.max(Number(text?.height || 0), 1)
|
|
859
|
+
const textBox = PcbScene3dTextBoxLayoutResolver.resolve(text)
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
...text,
|
|
863
|
+
...(textBox ? { textBox } : {}),
|
|
864
|
+
text: String(text?.text ?? text?.value ?? ''),
|
|
865
|
+
value: String(text?.text ?? text?.value ?? ''),
|
|
866
|
+
sizeX: height,
|
|
867
|
+
sizeY:
|
|
868
|
+
height *
|
|
869
|
+
PcbScene3dBuilder.#resolveSilkscreenTextWidthRatio(text),
|
|
870
|
+
thickness: Math.max(Number(text?.strokeWidth || 0), 1),
|
|
871
|
+
hAlign: 'left',
|
|
872
|
+
vAlign: 'bottom',
|
|
873
|
+
mirrored: PcbScene3dBuilder.#resolveSilkscreenTextMirrored(text),
|
|
874
|
+
side,
|
|
875
|
+
rotation: PcbScene3dBuilder.#resolveSilkscreenTextRotation(text)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Converts screen-space Altium text rotation for the shared 3D text factories.
|
|
881
|
+
* @param {{ rotation?: number | string }} text
|
|
882
|
+
* @returns {number}
|
|
883
|
+
*/
|
|
884
|
+
static #resolveSilkscreenTextRotation(text) {
|
|
541
885
|
return PcbScene3dBuilder.#normalizeAngle(
|
|
542
|
-
Number(
|
|
543
|
-
Number(componentBody?.rotationDeg || 0)
|
|
886
|
+
360 - Number(text?.rotation || 0)
|
|
544
887
|
)
|
|
545
888
|
}
|
|
546
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Checks whether one silkscreen text primitive uses TrueType glyphs.
|
|
892
|
+
* @param {{ fontTypeName?: string, fontType?: number | string, isTrueType?: boolean }} text
|
|
893
|
+
* @returns {boolean}
|
|
894
|
+
*/
|
|
895
|
+
static #isTrueTypeSilkscreenText(text) {
|
|
896
|
+
const fontTypeName = String(text?.fontTypeName || '').toUpperCase()
|
|
897
|
+
|
|
898
|
+
return (
|
|
899
|
+
text?.isTrueType === true ||
|
|
900
|
+
Number(text?.fontType) === 1 ||
|
|
901
|
+
fontTypeName.includes('TRUETYPE')
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Resolves the horizontal glyph scale used by the 3D stroke approximation.
|
|
907
|
+
* @param {{ fontTypeName?: string, fontType?: number | string, isTrueType?: boolean }} text
|
|
908
|
+
* @returns {number}
|
|
909
|
+
*/
|
|
910
|
+
static #resolveSilkscreenTextWidthRatio(text) {
|
|
911
|
+
return PcbScene3dBuilder.#isTrueTypeSilkscreenText(text)
|
|
912
|
+
? PcbScene3dBuilder.#TRUETYPE_TEXT_WIDTH_RATIO
|
|
913
|
+
: 1
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Resolves Altium's explicit per-text mirror flag.
|
|
918
|
+
* @param {{ mirrored?: boolean | number | string, isMirrored?: boolean | number | string, mirrorFlag?: boolean | number | string, Mirrored?: boolean | number | string, IsMirrored?: boolean | number | string, MirrorFlag?: boolean | number | string }} text
|
|
919
|
+
* @returns {boolean}
|
|
920
|
+
*/
|
|
921
|
+
static #resolveSilkscreenTextMirrored(text) {
|
|
922
|
+
const value =
|
|
923
|
+
text?.mirrored ??
|
|
924
|
+
text?.isMirrored ??
|
|
925
|
+
text?.mirrorFlag ??
|
|
926
|
+
text?.Mirrored ??
|
|
927
|
+
text?.IsMirrored ??
|
|
928
|
+
text?.MirrorFlag
|
|
929
|
+
|
|
930
|
+
if (typeof value === 'boolean') return value
|
|
931
|
+
if (typeof value === 'number') return value !== 0
|
|
932
|
+
|
|
933
|
+
return /^(1|true|yes|y)$/iu.test(String(value ?? '').trim())
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Detects dense Altium overlay regions that already carry text knockouts
|
|
938
|
+
* as native fill holes.
|
|
939
|
+
* @param {any[]} fills
|
|
940
|
+
* @param {{ tracks?: any[], arcs?: any[] }} primitives
|
|
941
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
942
|
+
* @returns {boolean}
|
|
943
|
+
*/
|
|
944
|
+
static #hasNativeTextKnockouts(fills, primitives, board) {
|
|
945
|
+
return (
|
|
946
|
+
(Boolean(primitives?.denseOverlayArtwork) ||
|
|
947
|
+
PcbScene3dBuilder.#isDenseOverlayArtwork(
|
|
948
|
+
{
|
|
949
|
+
fills,
|
|
950
|
+
tracks: primitives?.tracks,
|
|
951
|
+
arcs: primitives?.arcs
|
|
952
|
+
},
|
|
953
|
+
board
|
|
954
|
+
)) &&
|
|
955
|
+
(Array.isArray(fills) ? fills : []).some(
|
|
956
|
+
(fill) => Array.isArray(fill?.holes) && fill.holes.length > 0
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Detects overlay art from structural density rather than file-specific
|
|
963
|
+
* labels or source identifiers.
|
|
964
|
+
* @param {{ fills?: any[], tracks?: any[], arcs?: any[] }} side
|
|
965
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
966
|
+
* @returns {boolean}
|
|
967
|
+
*/
|
|
968
|
+
static #isDenseOverlayArtwork(side, board) {
|
|
969
|
+
const strokeCount =
|
|
970
|
+
(Array.isArray(side?.tracks) ? side.tracks.length : 0) +
|
|
971
|
+
(Array.isArray(side?.arcs) ? side.arcs.length : 0)
|
|
972
|
+
|
|
973
|
+
return (
|
|
974
|
+
strokeCount >= PcbScene3dBuilder.#DENSE_OVERLAY_MIN_TRACK_COUNT &&
|
|
975
|
+
PcbScene3dBuilder.#maxFillAreaRatio(side?.fills, board) >=
|
|
976
|
+
PcbScene3dBuilder.#DENSE_OVERLAY_MIN_REGION_AREA_RATIO
|
|
977
|
+
)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Resolves the largest fill-to-board bounding-box area ratio.
|
|
982
|
+
* @param {any[] | undefined} fills
|
|
983
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
984
|
+
* @returns {number}
|
|
985
|
+
*/
|
|
986
|
+
static #maxFillAreaRatio(fills, board) {
|
|
987
|
+
const boardArea =
|
|
988
|
+
Math.max(Number(board?.widthMil || 0), 0) *
|
|
989
|
+
Math.max(Number(board?.heightMil || 0), 0)
|
|
990
|
+
if (!boardArea) {
|
|
991
|
+
return 0
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return (Array.isArray(fills) ? fills : []).reduce((maxRatio, fill) => {
|
|
995
|
+
const bounds = PcbScene3dBuilder.#resolveFillBounds(fill)
|
|
996
|
+
if (!bounds) {
|
|
997
|
+
return maxRatio
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const fillArea =
|
|
1001
|
+
Math.max(bounds.maxX - bounds.minX, 0) *
|
|
1002
|
+
Math.max(bounds.maxY - bounds.minY, 0)
|
|
1003
|
+
|
|
1004
|
+
return Math.max(maxRatio, fillArea / boardArea)
|
|
1005
|
+
}, 0)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Resolves rough authored bounds for one rectangular or polygon fill.
|
|
1010
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, points?: { x?: number, y?: number }[] }} fill
|
|
1011
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
1012
|
+
*/
|
|
1013
|
+
static #resolveFillBounds(fill) {
|
|
1014
|
+
const points = Array.isArray(fill?.points)
|
|
1015
|
+
? fill.points
|
|
1016
|
+
.map((point) => ({
|
|
1017
|
+
x: Number(point?.x),
|
|
1018
|
+
y: Number(point?.y)
|
|
1019
|
+
}))
|
|
1020
|
+
.filter(
|
|
1021
|
+
(point) =>
|
|
1022
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
1023
|
+
)
|
|
1024
|
+
: [
|
|
1025
|
+
{ x: Number(fill?.x1), y: Number(fill?.y1) },
|
|
1026
|
+
{ x: Number(fill?.x2), y: Number(fill?.y2) }
|
|
1027
|
+
].filter(
|
|
1028
|
+
(point) =>
|
|
1029
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
if (points.length < 2) {
|
|
1033
|
+
return null
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const xs = points.map((point) => point.x)
|
|
1037
|
+
const ys = points.map((point) => point.y)
|
|
1038
|
+
|
|
1039
|
+
return {
|
|
1040
|
+
minX: Math.min(...xs),
|
|
1041
|
+
minY: Math.min(...ys),
|
|
1042
|
+
maxX: Math.max(...xs),
|
|
1043
|
+
maxY: Math.max(...ys)
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Resolves region primitives that can contribute filled silkscreen artwork.
|
|
1049
|
+
* @param {{ regions?: object[], shapeBasedRegions?: object[] }} pcb
|
|
1050
|
+
* @returns {object[]}
|
|
1051
|
+
*/
|
|
1052
|
+
static #resolveSilkscreenRegions(pcb) {
|
|
1053
|
+
if (
|
|
1054
|
+
Array.isArray(pcb?.shapeBasedRegions) &&
|
|
1055
|
+
pcb.shapeBasedRegions.length
|
|
1056
|
+
) {
|
|
1057
|
+
return pcb.shapeBasedRegions.map((region) => ({
|
|
1058
|
+
...region,
|
|
1059
|
+
isShapeBased: true
|
|
1060
|
+
}))
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return Array.isArray(pcb?.regions) ? pcb.regions : []
|
|
1064
|
+
}
|
|
1065
|
+
|
|
547
1066
|
/**
|
|
548
1067
|
* Resolves a rough pad-span box around one component.
|
|
549
1068
|
* @param {{ x: number, y: number }} component
|
|
@@ -619,85 +1138,6 @@ export class PcbScene3dBuilder {
|
|
|
619
1138
|
)
|
|
620
1139
|
}
|
|
621
1140
|
|
|
622
|
-
/**
|
|
623
|
-
* Resolves the grouping key for repeated component-body matching.
|
|
624
|
-
* @param {{ modelId?: string, name?: string, identifier?: string }} componentBody
|
|
625
|
-
* @returns {string}
|
|
626
|
-
*/
|
|
627
|
-
static #resolveBodyGroupKey(componentBody) {
|
|
628
|
-
return PcbScene3dBuilder.#normalizeLookupToken(
|
|
629
|
-
componentBody?.modelId ||
|
|
630
|
-
componentBody?.name ||
|
|
631
|
-
componentBody?.identifier
|
|
632
|
-
)
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Scores how strongly one component record appears to belong to one body
|
|
637
|
-
* record based on shared model/footprint tokens.
|
|
638
|
-
* @param {{ name?: string, identifier?: string }} componentBody
|
|
639
|
-
* @param {{ pattern?: string, source?: string, modelPath?: string }} component
|
|
640
|
-
* @returns {number}
|
|
641
|
-
*/
|
|
642
|
-
static #scoreBodyComponentAffinity(componentBody, component) {
|
|
643
|
-
const bodyTokens = PcbScene3dBuilder.#collectMeaningfulTokens([
|
|
644
|
-
componentBody?.identifier,
|
|
645
|
-
String(componentBody?.name || '').replace(/\.[^.]+$/, '')
|
|
646
|
-
])
|
|
647
|
-
const componentTokens = PcbScene3dBuilder.#collectMeaningfulTokens([
|
|
648
|
-
component?.pattern,
|
|
649
|
-
component?.source,
|
|
650
|
-
component?.modelPath
|
|
651
|
-
])
|
|
652
|
-
let score = 0
|
|
653
|
-
|
|
654
|
-
bodyTokens.forEach((token) => {
|
|
655
|
-
if (componentTokens.has(token)) {
|
|
656
|
-
score += token.length
|
|
657
|
-
}
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
return score
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Collects normalized model tokens from free-form strings.
|
|
665
|
-
* @param {(string | undefined)[]} values
|
|
666
|
-
* @returns {Set<string>}
|
|
667
|
-
*/
|
|
668
|
-
static #collectMeaningfulTokens(values) {
|
|
669
|
-
const tokens = new Set()
|
|
670
|
-
|
|
671
|
-
;(Array.isArray(values) ? values : []).forEach((value) => {
|
|
672
|
-
String(value || '')
|
|
673
|
-
.toLowerCase()
|
|
674
|
-
.split(/[^a-z0-9]+/g)
|
|
675
|
-
.forEach((fragment) => {
|
|
676
|
-
;(fragment.match(/[a-z]+|\d+/g) || []).forEach((token) => {
|
|
677
|
-
if (PcbScene3dBuilder.#isMeaningfulToken(token)) {
|
|
678
|
-
tokens.add(token)
|
|
679
|
-
}
|
|
680
|
-
})
|
|
681
|
-
})
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
return tokens
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Returns true when one normalized token carries useful model identity.
|
|
689
|
-
* @param {string} token
|
|
690
|
-
* @returns {boolean}
|
|
691
|
-
*/
|
|
692
|
-
static #isMeaningfulToken(token) {
|
|
693
|
-
return (
|
|
694
|
-
String(token || '').length >= 2 &&
|
|
695
|
-
!new Set(['con', 'step', 'stp', 'model', 'default', 'black']).has(
|
|
696
|
-
String(token || '')
|
|
697
|
-
)
|
|
698
|
-
)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
1141
|
/**
|
|
702
1142
|
* Returns true when one unresolved body anchor still lies close enough to
|
|
703
1143
|
* the board envelope to be renderable without a component match.
|
|
@@ -718,17 +1158,6 @@ export class PcbScene3dBuilder {
|
|
|
718
1158
|
return bodyX >= minX && bodyX <= maxX && bodyY >= minY && bodyY <= maxY
|
|
719
1159
|
}
|
|
720
1160
|
|
|
721
|
-
/**
|
|
722
|
-
* Normalizes one lookup token for repeated-model grouping.
|
|
723
|
-
* @param {string | undefined} value
|
|
724
|
-
* @returns {string}
|
|
725
|
-
*/
|
|
726
|
-
static #normalizeLookupToken(value) {
|
|
727
|
-
return String(value || '')
|
|
728
|
-
.toLowerCase()
|
|
729
|
-
.replace(/[^a-z0-9]+/g, '')
|
|
730
|
-
}
|
|
731
|
-
|
|
732
1161
|
/**
|
|
733
1162
|
* Normalizes one angle into the range [0, 360).
|
|
734
1163
|
* @param {number} angle
|