pcb-scene3d-viewer 1.1.0 → 1.1.2
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 +10 -0
- package/package.json +2 -2
- package/src/PcbScene3dAdjustmentControlBinder.mjs +667 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +1 -1
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +1 -1
- package/src/PcbScene3dCircularCutoutOverlap.mjs +167 -0
- package/src/PcbScene3dComponentAdjustment.mjs +174 -0
- package/src/PcbScene3dComponentAdjustmentRegistry.mjs +95 -0
- package/src/PcbScene3dController.mjs +214 -197
- package/src/PcbScene3dCopperFactory.mjs +4 -1
- package/src/PcbScene3dCutoutGeometryFilter.mjs +18 -3
- package/src/PcbScene3dExternalModels.mjs +56 -71
- package/src/PcbScene3dGeometryBoundsResolver.mjs +78 -0
- package/src/PcbScene3dModelBounds.mjs +267 -0
- package/src/PcbScene3dModelSearchPlacement.mjs +40 -0
- package/src/PcbScene3dPadFactory.mjs +221 -20
- package/src/PcbScene3dRenderGroupVisibility.mjs +23 -1
- package/src/PcbScene3dRuntime.mjs +62 -51
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +581 -0
- package/src/PcbScene3dSelectionResolver.mjs +42 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +4 -1
- package/src/PcbScene3dText.mjs +3 -0
- package/src/styles/scene3d.css +142 -0
|
@@ -27,18 +27,28 @@ export class PcbScene3dPadFactory {
|
|
|
27
27
|
const geometryCache = new Map()
|
|
28
28
|
const side = PcbScene3dPadFactory.#normalizeSide(options?.side)
|
|
29
29
|
const mirrorY = Boolean(options?.mirrorY)
|
|
30
|
+
const normalizedPads = pads || []
|
|
31
|
+
const boardDrills = PcbScene3dDrillPathFactory.resolveBoardDrillSpecs({
|
|
32
|
+
pads: normalizedPads
|
|
33
|
+
})
|
|
30
34
|
|
|
31
|
-
;(
|
|
35
|
+
;(normalizedPads || []).forEach((pad) => {
|
|
32
36
|
if (!PcbScene3dPadFactory.#hasVisibleSurface(pad, side)) {
|
|
33
37
|
return
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
const spec = PcbScene3dPadFactory.resolvePadSurfaceSpec(pad, side)
|
|
41
|
+
const drillCutouts = PcbScene3dPadFactory.#resolvePadDrillCutouts(
|
|
42
|
+
pad,
|
|
43
|
+
spec,
|
|
44
|
+
boardDrills,
|
|
45
|
+
mirrorY
|
|
46
|
+
)
|
|
37
47
|
const geometry = PcbScene3dPadFactory.#resolveGeometry(
|
|
38
48
|
THREE,
|
|
39
49
|
geometryCache,
|
|
40
50
|
spec,
|
|
41
|
-
|
|
51
|
+
drillCutouts
|
|
42
52
|
)
|
|
43
53
|
const point = PcbScene3dPadFactory.#normalizePoint(
|
|
44
54
|
normalizeBoardPoint,
|
|
@@ -391,27 +401,22 @@ export class PcbScene3dPadFactory {
|
|
|
391
401
|
* Resolves or creates one reusable geometry for the pad spec.
|
|
392
402
|
* @param {any} THREE
|
|
393
403
|
* @param {Map<string, any>} geometryCache
|
|
394
|
-
* @param {any} pad
|
|
395
404
|
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number, cornerRadius: number, hasHole: boolean, holeDiameter: number, holeSlotLength: number | null, holeRotation: number }} spec
|
|
405
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]} drillCutouts
|
|
396
406
|
* @returns {any}
|
|
397
407
|
*/
|
|
398
|
-
static #resolveGeometry(THREE, geometryCache, spec,
|
|
399
|
-
const cacheKey =
|
|
400
|
-
spec
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
spec.cornerRadius.toFixed(4),
|
|
404
|
-
spec.holeDiameter.toFixed(4),
|
|
405
|
-
Number(spec.holeSlotLength || 0).toFixed(4),
|
|
406
|
-
spec.holeRotation.toFixed(4)
|
|
407
|
-
].join(':')
|
|
408
|
+
static #resolveGeometry(THREE, geometryCache, spec, drillCutouts) {
|
|
409
|
+
const cacheKey = PcbScene3dPadFactory.#buildGeometryCacheKey(
|
|
410
|
+
spec,
|
|
411
|
+
drillCutouts
|
|
412
|
+
)
|
|
408
413
|
const cached = geometryCache.get(cacheKey)
|
|
409
414
|
if (cached) {
|
|
410
415
|
return cached
|
|
411
416
|
}
|
|
412
417
|
|
|
413
418
|
let geometry
|
|
414
|
-
if (spec.kind === 'circle' && !
|
|
419
|
+
if (spec.kind === 'circle' && !drillCutouts.length) {
|
|
415
420
|
geometry = new THREE.CylinderGeometry(
|
|
416
421
|
spec.radius,
|
|
417
422
|
spec.radius,
|
|
@@ -420,12 +425,14 @@ export class PcbScene3dPadFactory {
|
|
|
420
425
|
)
|
|
421
426
|
} else {
|
|
422
427
|
const shape = PcbScene3dPadFactory.#buildOuterShape(THREE, spec)
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
428
|
+
for (const drillCutout of drillCutouts) {
|
|
429
|
+
const drillHole = PcbScene3dDrillPathFactory.buildDrillPath(
|
|
430
|
+
THREE,
|
|
431
|
+
drillCutout
|
|
432
|
+
)
|
|
433
|
+
if (drillHole) {
|
|
434
|
+
shape.holes.push(drillHole)
|
|
435
|
+
}
|
|
429
436
|
}
|
|
430
437
|
geometry = new THREE.ExtrudeGeometry(shape, {
|
|
431
438
|
depth: PcbScene3dPadFactory.#PAD_THICKNESS_MIL,
|
|
@@ -444,6 +451,200 @@ export class PcbScene3dPadFactory {
|
|
|
444
451
|
return geometry
|
|
445
452
|
}
|
|
446
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Resolves drill apertures that intersect one pad's local copper face.
|
|
456
|
+
* @param {{ x?: number, y?: number, rotation?: number | null }} pad
|
|
457
|
+
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number, offsetX: number, offsetY: number }} spec
|
|
458
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]} boardDrills
|
|
459
|
+
* @param {boolean} mirrorY
|
|
460
|
+
* @returns {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]}
|
|
461
|
+
*/
|
|
462
|
+
static #resolvePadDrillCutouts(pad, spec, boardDrills, mirrorY) {
|
|
463
|
+
return (boardDrills || [])
|
|
464
|
+
.map((drillSpec) =>
|
|
465
|
+
PcbScene3dPadFactory.#toPadLocalDrillCutout(
|
|
466
|
+
pad,
|
|
467
|
+
spec,
|
|
468
|
+
drillSpec,
|
|
469
|
+
mirrorY
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
.filter((drillSpec) =>
|
|
473
|
+
PcbScene3dPadFactory.#drillTouchesPadSurface(drillSpec, spec)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Converts one board-space drill aperture into pad-local shape space.
|
|
479
|
+
* @param {{ x?: number, y?: number, rotation?: number | null }} pad
|
|
480
|
+
* @param {{ offsetX: number, offsetY: number }} spec
|
|
481
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
|
|
482
|
+
* @param {boolean} mirrorY
|
|
483
|
+
* @returns {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }}
|
|
484
|
+
*/
|
|
485
|
+
static #toPadLocalDrillCutout(pad, spec, drillSpec, mirrorY) {
|
|
486
|
+
const padRotationDeg = Number(pad?.rotation || 0)
|
|
487
|
+
const relative = {
|
|
488
|
+
x: Number(drillSpec?.x || 0) - Number(pad?.x || 0),
|
|
489
|
+
y: Number(drillSpec?.y || 0) - Number(pad?.y || 0)
|
|
490
|
+
}
|
|
491
|
+
if (mirrorY) {
|
|
492
|
+
relative.y *= -1
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const local = PcbScene3dPadFactory.#rotatePoint(
|
|
496
|
+
relative,
|
|
497
|
+
-padRotationDeg
|
|
498
|
+
)
|
|
499
|
+
const localOffsetY = mirrorY ? -spec.offsetY : spec.offsetY
|
|
500
|
+
const diameter = Number(drillSpec?.diameter || 0)
|
|
501
|
+
const slotLength =
|
|
502
|
+
Number(drillSpec?.slotLength || 0) > diameter
|
|
503
|
+
? Number(drillSpec.slotLength || 0)
|
|
504
|
+
: null
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
x: local.x - spec.offsetX,
|
|
508
|
+
y: local.y - localOffsetY,
|
|
509
|
+
diameter,
|
|
510
|
+
slotLength,
|
|
511
|
+
rotationDeg: PcbScene3dPadFactory.#normalizeAngle(
|
|
512
|
+
Number(drillSpec?.rotationDeg || 0) - padRotationDeg
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Checks whether a local drill aperture overlaps the pad's surface bounds.
|
|
519
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
|
|
520
|
+
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number }} spec
|
|
521
|
+
* @returns {boolean}
|
|
522
|
+
*/
|
|
523
|
+
static #drillTouchesPadSurface(drillSpec, spec) {
|
|
524
|
+
if (Number(drillSpec?.diameter || 0) <= 0) {
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const bounds = PcbScene3dPadFactory.#resolveDrillBounds(drillSpec)
|
|
529
|
+
const halfWidth = Number(spec.width || 0) / 2
|
|
530
|
+
const halfHeight = Number(spec.height || 0) / 2
|
|
531
|
+
const boundsOverlap =
|
|
532
|
+
bounds.maxX >= -halfWidth &&
|
|
533
|
+
bounds.minX <= halfWidth &&
|
|
534
|
+
bounds.maxY >= -halfHeight &&
|
|
535
|
+
bounds.minY <= halfHeight
|
|
536
|
+
if (!boundsOverlap) {
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (spec.kind !== 'circle') {
|
|
541
|
+
return true
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
Math.hypot(Number(drillSpec.x || 0), Number(drillSpec.y || 0)) <=
|
|
546
|
+
Number(spec.radius || 0) +
|
|
547
|
+
PcbScene3dPadFactory.#resolveDrillReach(drillSpec)
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Resolves the axis-aligned local bounds of one drill aperture.
|
|
553
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
|
|
554
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
|
|
555
|
+
*/
|
|
556
|
+
static #resolveDrillBounds(drillSpec) {
|
|
557
|
+
const radius = Math.max(Number(drillSpec.diameter || 0) / 2, 0)
|
|
558
|
+
const halfTrack = Math.max(
|
|
559
|
+
(Number(drillSpec.slotLength || 0) -
|
|
560
|
+
Number(drillSpec.diameter || 0)) /
|
|
561
|
+
2,
|
|
562
|
+
0
|
|
563
|
+
)
|
|
564
|
+
const rotationRad = (Number(drillSpec.rotationDeg || 0) * Math.PI) / 180
|
|
565
|
+
const extentX = Math.abs(Math.cos(rotationRad)) * halfTrack + radius
|
|
566
|
+
const extentY = Math.abs(Math.sin(rotationRad)) * halfTrack + radius
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
minX: Number(drillSpec.x || 0) - extentX,
|
|
570
|
+
maxX: Number(drillSpec.x || 0) + extentX,
|
|
571
|
+
minY: Number(drillSpec.y || 0) - extentY,
|
|
572
|
+
maxY: Number(drillSpec.y || 0) + extentY
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Resolves a conservative radial reach for one drill aperture.
|
|
578
|
+
* @param {{ diameter: number, slotLength?: number | null }} drillSpec
|
|
579
|
+
* @returns {number}
|
|
580
|
+
*/
|
|
581
|
+
static #resolveDrillReach(drillSpec) {
|
|
582
|
+
return (
|
|
583
|
+
Math.max(
|
|
584
|
+
Number(drillSpec.diameter || 0),
|
|
585
|
+
Number(drillSpec.slotLength || 0)
|
|
586
|
+
) / 2
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Rotates one 2D point around the origin.
|
|
592
|
+
* @param {{ x: number, y: number }} point
|
|
593
|
+
* @param {number} angleDeg
|
|
594
|
+
* @returns {{ x: number, y: number }}
|
|
595
|
+
*/
|
|
596
|
+
static #rotatePoint(point, angleDeg) {
|
|
597
|
+
const angleRad = (Number(angleDeg || 0) * Math.PI) / 180
|
|
598
|
+
const cos = Math.cos(angleRad)
|
|
599
|
+
const sin = Math.sin(angleRad)
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
x: point.x * cos - point.y * sin,
|
|
603
|
+
y: point.x * sin + point.y * cos
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Builds a cache key that includes pad geometry and local drill cutouts.
|
|
609
|
+
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', cornerRadius: number, holeDiameter: number, holeSlotLength: number | null, holeRotation: number }} spec
|
|
610
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]} drillCutouts
|
|
611
|
+
* @returns {string}
|
|
612
|
+
*/
|
|
613
|
+
static #buildGeometryCacheKey(spec, drillCutouts) {
|
|
614
|
+
const drillKey = (drillCutouts || [])
|
|
615
|
+
.map((drillSpec) =>
|
|
616
|
+
[
|
|
617
|
+
Number(drillSpec.x || 0).toFixed(4),
|
|
618
|
+
Number(drillSpec.y || 0).toFixed(4),
|
|
619
|
+
Number(drillSpec.diameter || 0).toFixed(4),
|
|
620
|
+
Number(drillSpec.slotLength || 0).toFixed(4),
|
|
621
|
+
Number(drillSpec.rotationDeg || 0).toFixed(4)
|
|
622
|
+
].join(',')
|
|
623
|
+
)
|
|
624
|
+
.join('|')
|
|
625
|
+
|
|
626
|
+
return [
|
|
627
|
+
spec.kind,
|
|
628
|
+
spec.width.toFixed(4),
|
|
629
|
+
spec.height.toFixed(4),
|
|
630
|
+
spec.cornerRadius.toFixed(4),
|
|
631
|
+
spec.holeDiameter.toFixed(4),
|
|
632
|
+
Number(spec.holeSlotLength || 0).toFixed(4),
|
|
633
|
+
spec.holeRotation.toFixed(4),
|
|
634
|
+
drillKey
|
|
635
|
+
].join(':')
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Normalizes one angle into the inclusive `[0, 360)` range.
|
|
640
|
+
* @param {number} angleDeg
|
|
641
|
+
* @returns {number}
|
|
642
|
+
*/
|
|
643
|
+
static #normalizeAngle(angleDeg) {
|
|
644
|
+
const normalized = Number(angleDeg || 0) % 360
|
|
645
|
+
return normalized < 0 ? normalized + 360 : normalized
|
|
646
|
+
}
|
|
647
|
+
|
|
447
648
|
/**
|
|
448
649
|
* Builds one pad outline shape for extrusion.
|
|
449
650
|
* @param {any} THREE
|
|
@@ -9,7 +9,7 @@ export class PcbScene3dRenderGroupVisibility {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Applies the active 3D detail visibility state.
|
|
12
|
-
* @param {{ groups: Map<string, any>, toggles: { 'external-models': boolean, 'fallback-bodies': boolean, copper: boolean }, fallbackBodyRoots: Map<string, Set<any>>, loadedExternalModelDesignators: Set<string>, hasLoadedBoardAssemblyModel?: boolean }} state Visibility state.
|
|
12
|
+
* @param {{ groups: Map<string, any>, toggles: { 'external-models': boolean, 'fallback-bodies': boolean, 'model-search-models'?: boolean, copper: boolean }, fallbackBodyRoots: Map<string, Set<any>>, loadedExternalModelDesignators: Set<string>, modelSearchExternalModelRoots?: Set<any>, hasLoadedBoardAssemblyModel?: boolean }} state Visibility state.
|
|
13
13
|
* @returns {void}
|
|
14
14
|
*/
|
|
15
15
|
static apply(state) {
|
|
@@ -51,6 +51,28 @@ export class PcbScene3dRenderGroupVisibility {
|
|
|
51
51
|
state?.groups?.get('external-models'),
|
|
52
52
|
Boolean(state?.toggles?.['external-models'])
|
|
53
53
|
)
|
|
54
|
+
PcbScene3dRenderGroupVisibility.#applyModelSearchExternalVisibility(
|
|
55
|
+
state
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Applies the app-discovered model toggle to individual external roots.
|
|
61
|
+
* @param {{ toggles?: { 'external-models'?: boolean, 'model-search-models'?: boolean }, modelSearchExternalModelRoots?: Set<any> }} state Visibility state.
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
static #applyModelSearchExternalVisibility(state) {
|
|
65
|
+
const roots = state?.modelSearchExternalModelRoots
|
|
66
|
+
if (!roots || typeof roots.forEach !== 'function') {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const visible =
|
|
71
|
+
Boolean(state?.toggles?.['external-models']) &&
|
|
72
|
+
state?.toggles?.['model-search-models'] !== false
|
|
73
|
+
roots.forEach((root) => {
|
|
74
|
+
PcbScene3dRenderGroupVisibility.#setVisible(root, visible)
|
|
75
|
+
})
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
/**
|
|
@@ -2,6 +2,8 @@ import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFac
|
|
|
2
2
|
import { PcbScene3dBodyColor } from './PcbScene3dBodyColor.mjs'
|
|
3
3
|
import { PcbScene3dCameraRig } from './PcbScene3dCameraRig.mjs'
|
|
4
4
|
import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
|
|
5
|
+
import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
|
|
6
|
+
import { PcbScene3dComponentAdjustmentRegistry } from './PcbScene3dComponentAdjustmentRegistry.mjs'
|
|
5
7
|
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
6
8
|
import { PcbScene3dCopperDetailFilter } from './PcbScene3dCopperDetailFilter.mjs'
|
|
7
9
|
import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
|
|
@@ -10,15 +12,18 @@ import { PcbScene3dExternalModels } from './PcbScene3dExternalModels.mjs'
|
|
|
10
12
|
import { PcbScene3dFallbackVisibility } from './PcbScene3dFallbackVisibility.mjs'
|
|
11
13
|
import { PcbScene3dInteractionHints } from './PcbScene3dInteractionHints.mjs'
|
|
12
14
|
import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
|
|
15
|
+
import { PcbScene3dModelSearchPlacement } from './PcbScene3dModelSearchPlacement.mjs'
|
|
13
16
|
import { PcbScene3dPresetState } from './PcbScene3dPresetState.mjs'
|
|
14
17
|
import { PcbScene3dRenderGroupVisibility } from './PcbScene3dRenderGroupVisibility.mjs'
|
|
15
18
|
import { PcbScene3dRuntimeBoardMeshes } from './PcbScene3dRuntimeBoardMeshes.mjs'
|
|
16
19
|
import { PcbScene3dSilkscreenChunkedFactory } from './PcbScene3dSilkscreenChunkedFactory.mjs'
|
|
17
20
|
import { PcbScene3dTrueTypeTextFactory } from './PcbScene3dTrueTypeTextFactory.mjs'
|
|
21
|
+
import { PcbScene3dSelectionResolver } from './PcbScene3dSelectionResolver.mjs'
|
|
18
22
|
import { PcbScene3dSelectionStyler } from './PcbScene3dSelectionStyler.mjs'
|
|
19
23
|
import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
|
|
20
24
|
import { PcbScene3dViewportResize } from './PcbScene3dViewportResize.mjs'
|
|
21
25
|
import { PcbScene3dViewScale } from './PcbScene3dViewScale.mjs'
|
|
26
|
+
const Z_MIL = { cu: 0.05, silk: 0.06 }
|
|
22
27
|
/**
|
|
23
28
|
* Browser-side Three.js runtime for the interactive PCB 3D viewport.
|
|
24
29
|
*/
|
|
@@ -29,7 +34,7 @@ export class PcbScene3dRuntime {
|
|
|
29
34
|
#sceneDescription
|
|
30
35
|
/** @type {{ setDiagnostics?: (messages: string[]) => void, setSelection?: (selection: any | null) => void, loadRuntimeModules?: () => Promise<{ THREE: any, OrbitControls: any }>, translate?: ((key: string) => string) | null }} */
|
|
31
36
|
#hooks
|
|
32
|
-
/** @type {{ 'external-models': boolean, 'fallback-bodies': boolean, copper: boolean }} */
|
|
37
|
+
/** @type {{ 'external-models': boolean, 'fallback-bodies': boolean, 'model-search-models': boolean, copper: boolean }} */
|
|
33
38
|
#toggles
|
|
34
39
|
/** @type {Map<string, any>} */
|
|
35
40
|
#groups
|
|
@@ -61,10 +66,14 @@ export class PcbScene3dRuntime {
|
|
|
61
66
|
#pointerDownPosition
|
|
62
67
|
/** @type {Map<string, Set<any>>} */
|
|
63
68
|
#selectionRoots
|
|
69
|
+
/** @type {PcbScene3dComponentAdjustmentRegistry} */
|
|
70
|
+
#componentAdjustmentRegistry
|
|
64
71
|
/** @type {Map<string, Set<any>>} */
|
|
65
72
|
#fallbackBodyRoots
|
|
66
73
|
/** @type {Set<string>} */
|
|
67
74
|
#loadedExternalModelDesignators
|
|
75
|
+
/** @type {Set<any>} */
|
|
76
|
+
#modelSearchExternalModelRoots
|
|
68
77
|
/** @type {boolean} */
|
|
69
78
|
#hasLoadedBoardAssemblyModel
|
|
70
79
|
/** @type {string} */
|
|
@@ -81,7 +90,6 @@ export class PcbScene3dRuntime {
|
|
|
81
90
|
#resolveReadyPromise
|
|
82
91
|
/** @type {boolean} */
|
|
83
92
|
#hasSettledReady
|
|
84
|
-
|
|
85
93
|
/**
|
|
86
94
|
* @param {HTMLElement} viewportNode
|
|
87
95
|
* @param {any} sceneDescription Scene description or CircuitJSON model.
|
|
@@ -96,6 +104,7 @@ export class PcbScene3dRuntime {
|
|
|
96
104
|
this.#toggles = {
|
|
97
105
|
'external-models': true,
|
|
98
106
|
'fallback-bodies': false,
|
|
107
|
+
'model-search-models': true,
|
|
99
108
|
copper: true
|
|
100
109
|
}
|
|
101
110
|
this.#groups = new Map()
|
|
@@ -113,8 +122,11 @@ export class PcbScene3dRuntime {
|
|
|
113
122
|
this.#pointer = null
|
|
114
123
|
this.#pointerDownPosition = null
|
|
115
124
|
this.#selectionRoots = new Map()
|
|
125
|
+
this.#componentAdjustmentRegistry =
|
|
126
|
+
new PcbScene3dComponentAdjustmentRegistry(() => this.#three)
|
|
116
127
|
this.#fallbackBodyRoots = new Map()
|
|
117
128
|
this.#loadedExternalModelDesignators = new Set()
|
|
129
|
+
this.#modelSearchExternalModelRoots = new Set()
|
|
118
130
|
this.#hasLoadedBoardAssemblyModel = false
|
|
119
131
|
this.#selectedDesignator = ''
|
|
120
132
|
this.#initialRadius =
|
|
@@ -134,7 +146,6 @@ export class PcbScene3dRuntime {
|
|
|
134
146
|
])
|
|
135
147
|
this.#initialize()
|
|
136
148
|
}
|
|
137
|
-
|
|
138
149
|
/** @param {string} preset */
|
|
139
150
|
setPreset(preset) {
|
|
140
151
|
const normalizedPreset = this.#presetState.set(preset)
|
|
@@ -151,7 +162,6 @@ export class PcbScene3dRuntime {
|
|
|
151
162
|
)
|
|
152
163
|
this.#render()
|
|
153
164
|
}
|
|
154
|
-
|
|
155
165
|
/** @param {string} toggleName @param {boolean} enabled */
|
|
156
166
|
setToggle(toggleName, enabled) {
|
|
157
167
|
if (!(toggleName in this.#toggles)) {
|
|
@@ -168,6 +178,18 @@ export class PcbScene3dRuntime {
|
|
|
168
178
|
this.#setSelectedDesignator(designator)
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Applies a live model-local adjustment to one component.
|
|
183
|
+
* @param {string} designator Component designator.
|
|
184
|
+
* @param {{ scale?: { x?: number, y?: number, z?: number }, rotationDeg?: { x?: number, y?: number, z?: number }, offsetMil?: { x?: number, y?: number, z?: number } }} adjustment Transform adjustment.
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
setComponentAdjustment(designator, adjustment) {
|
|
188
|
+
if (this.#componentAdjustmentRegistry.set(designator, adjustment)) {
|
|
189
|
+
this.#render()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
/**
|
|
172
194
|
* Resolves when the runtime has completed its initial async
|
|
173
195
|
* initialization and deferred scene settlement.
|
|
@@ -204,8 +226,10 @@ export class PcbScene3dRuntime {
|
|
|
204
226
|
this.#pointer = null
|
|
205
227
|
this.#pointerDownPosition = null
|
|
206
228
|
this.#selectionRoots.clear()
|
|
229
|
+
this.#componentAdjustmentRegistry.clear()
|
|
207
230
|
this.#fallbackBodyRoots.clear()
|
|
208
231
|
this.#loadedExternalModelDesignators.clear()
|
|
232
|
+
this.#modelSearchExternalModelRoots.clear()
|
|
209
233
|
this.#hasLoadedBoardAssemblyModel = false
|
|
210
234
|
this.#selectedDesignator = ''
|
|
211
235
|
this.#groups.clear()
|
|
@@ -464,18 +488,18 @@ export class PcbScene3dRuntime {
|
|
|
464
488
|
await PcbScene3dTrueTypeTextFactory.prepareEmbeddedFonts(
|
|
465
489
|
this.#sceneDescription.detail.embeddedFonts || []
|
|
466
490
|
)
|
|
491
|
+
const topZ = this.#sceneDescription.board.thicknessMil / 2 + Z_MIL.silk
|
|
467
492
|
const detailGroup = await PcbScene3dSilkscreenChunkedFactory.buildGroup(
|
|
468
493
|
this.#three,
|
|
469
494
|
this.#sceneDescription.detail.silkscreen || {},
|
|
470
|
-
|
|
471
|
-
-
|
|
495
|
+
topZ,
|
|
496
|
+
-topZ,
|
|
472
497
|
(x, y) => this.#normalizeDetailPoint(x, y),
|
|
473
498
|
{
|
|
474
499
|
shouldContinue: () => !this.#isDisposed,
|
|
475
500
|
yieldToMain: () => PcbScene3dRuntime.#yieldToNextFrame()
|
|
476
501
|
}
|
|
477
502
|
)
|
|
478
|
-
|
|
479
503
|
if (this.#isDisposed) return
|
|
480
504
|
if (detailGroup.children.length) {
|
|
481
505
|
silkscreenGroup.add(detailGroup)
|
|
@@ -492,15 +516,15 @@ export class PcbScene3dRuntime {
|
|
|
492
516
|
if (!copperGroup || copperGroup.children.length) {
|
|
493
517
|
return
|
|
494
518
|
}
|
|
495
|
-
|
|
496
519
|
const copperDetail = PcbScene3dCopperDetailFilter.resolve(
|
|
497
520
|
this.#sceneDescription
|
|
498
521
|
)
|
|
522
|
+
const topZ = this.#sceneDescription.board.thicknessMil / 2 + Z_MIL.cu
|
|
499
523
|
const detailGroup = PcbScene3dCopperFactory.buildGroup(
|
|
500
524
|
this.#three,
|
|
501
525
|
copperDetail,
|
|
502
|
-
|
|
503
|
-
-
|
|
526
|
+
topZ,
|
|
527
|
+
-topZ,
|
|
504
528
|
(x, y) => this.#normalizeDetailPoint(x, y),
|
|
505
529
|
{ coordinateSystem: this.#sceneDescription.coordinateSystem }
|
|
506
530
|
)
|
|
@@ -564,14 +588,25 @@ export class PcbScene3dRuntime {
|
|
|
564
588
|
|
|
565
589
|
const mountRig = PcbScene3dMountRig.create(THREE, component)
|
|
566
590
|
const rootGroup = mountRig.rootGroup
|
|
591
|
+
const adjustmentGroup = new THREE.Group()
|
|
567
592
|
|
|
568
593
|
rootGroup.userData.scene3dSelection = {
|
|
569
594
|
designator: String(component?.designator || 'component'),
|
|
570
595
|
sourceType: 'component'
|
|
571
596
|
}
|
|
597
|
+
adjustmentGroup.userData.scene3dAdjustmentTarget = true
|
|
598
|
+
adjustmentGroup.userData.scene3dAdjustmentDesignator = String(
|
|
599
|
+
component?.designator || 'component'
|
|
600
|
+
)
|
|
601
|
+
adjustmentGroup.userData.scene3dAdjustmentBaseline =
|
|
602
|
+
PcbScene3dComponentAdjustment.neutral()
|
|
572
603
|
this.#registerSelectionRoot(component?.designator, rootGroup)
|
|
573
|
-
|
|
574
|
-
|
|
604
|
+
this.#componentAdjustmentRegistry.register(
|
|
605
|
+
component?.designator,
|
|
606
|
+
adjustmentGroup
|
|
607
|
+
)
|
|
608
|
+
adjustmentGroup.add(mesh)
|
|
609
|
+
mountRig.faceGroup.add(adjustmentGroup)
|
|
575
610
|
return rootGroup
|
|
576
611
|
}
|
|
577
612
|
|
|
@@ -599,6 +634,19 @@ export class PcbScene3dRuntime {
|
|
|
599
634
|
placement?.designator,
|
|
600
635
|
placementGroup
|
|
601
636
|
)
|
|
637
|
+
PcbScene3dComponentAdjustment.findTargets(
|
|
638
|
+
placementGroup
|
|
639
|
+
).forEach((target) => {
|
|
640
|
+
this.#componentAdjustmentRegistry.register(
|
|
641
|
+
placement?.designator,
|
|
642
|
+
target
|
|
643
|
+
)
|
|
644
|
+
})
|
|
645
|
+
PcbScene3dModelSearchPlacement.registerRoot(
|
|
646
|
+
placement,
|
|
647
|
+
placementGroup,
|
|
648
|
+
this.#modelSearchExternalModelRoots
|
|
649
|
+
)
|
|
602
650
|
if (
|
|
603
651
|
String(placement?.sourceType || '').toLowerCase() ===
|
|
604
652
|
'board-assembly'
|
|
@@ -728,6 +776,7 @@ export class PcbScene3dRuntime {
|
|
|
728
776
|
fallbackBodyRoots: this.#fallbackBodyRoots,
|
|
729
777
|
loadedExternalModelDesignators:
|
|
730
778
|
this.#loadedExternalModelDesignators,
|
|
779
|
+
modelSearchExternalModelRoots: this.#modelSearchExternalModelRoots,
|
|
731
780
|
hasLoadedBoardAssemblyModel: this.#hasLoadedBoardAssemblyModel
|
|
732
781
|
})
|
|
733
782
|
}
|
|
@@ -850,7 +899,7 @@ export class PcbScene3dRuntime {
|
|
|
850
899
|
true
|
|
851
900
|
)
|
|
852
901
|
const selection =
|
|
853
|
-
|
|
902
|
+
PcbScene3dSelectionResolver.fromIntersections(intersections)
|
|
854
903
|
this.#setSelectedDesignator(selection?.designator)
|
|
855
904
|
this.#hooks.setSelection?.(selection)
|
|
856
905
|
}
|
|
@@ -930,44 +979,6 @@ export class PcbScene3dRuntime {
|
|
|
930
979
|
return PcbScene3dViewScale.resolve(preset, sceneDescription)
|
|
931
980
|
}
|
|
932
981
|
|
|
933
|
-
/**
|
|
934
|
-
* Resolves the picked selection record from raycast intersections.
|
|
935
|
-
* @param {{ object?: any }[]} intersections
|
|
936
|
-
* @returns {{ designator?: string, sourceType?: string } | null}
|
|
937
|
-
*/
|
|
938
|
-
static #resolveSelectionFromIntersections(intersections) {
|
|
939
|
-
for (const intersection of intersections) {
|
|
940
|
-
const selection = PcbScene3dRuntime.#resolveSelectionFromObject(
|
|
941
|
-
intersection?.object
|
|
942
|
-
)
|
|
943
|
-
if (selection) {
|
|
944
|
-
return selection
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
return null
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Resolves one selection payload by walking up a picked object tree.
|
|
953
|
-
* @param {any} object
|
|
954
|
-
* @returns {{ designator?: string, sourceType?: string } | null}
|
|
955
|
-
*/
|
|
956
|
-
static #resolveSelectionFromObject(object) {
|
|
957
|
-
let current = object
|
|
958
|
-
|
|
959
|
-
while (current) {
|
|
960
|
-
const selection = current?.userData?.scene3dSelection
|
|
961
|
-
if (selection) {
|
|
962
|
-
return selection
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
current = current.parent || null
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
return null
|
|
969
|
-
}
|
|
970
|
-
|
|
971
982
|
/** @returns {number} */
|
|
972
983
|
static #resolveSelectionHighlightColor() {
|
|
973
984
|
return 0x14c5e6
|