pcb-scene3d-viewer 1.1.49 → 1.2.0
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/README.md +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PcbScene3dAabbIndex } from './PcbScene3dAabbIndex.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request-scoped broad-phase index for prepared polygons.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dPreparedPolygonSet {
|
|
7
|
+
/** @type {PcbScene3dAabbIndex} */
|
|
8
|
+
#index
|
|
9
|
+
|
|
10
|
+
/** @type {Map<*, *>} */
|
|
11
|
+
#sourceMap
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds a set index while retaining supplied prepared object identities.
|
|
15
|
+
* @param {Iterable<{ source: *, bounds: { minX: number, maxX: number, minY: number, maxY: number } }>} polygons
|
|
16
|
+
*/
|
|
17
|
+
constructor(polygons) {
|
|
18
|
+
const preparedPolygons = Array.from(polygons || [])
|
|
19
|
+
|
|
20
|
+
this.#index = new PcbScene3dAabbIndex(preparedPolygons, {
|
|
21
|
+
resolveBounds: PcbScene3dPreparedPolygonSet.#resolveBounds,
|
|
22
|
+
resolveSourceIndex: PcbScene3dPreparedPolygonSet.#resolveSetPosition
|
|
23
|
+
})
|
|
24
|
+
this.#sourceMap = new Map()
|
|
25
|
+
|
|
26
|
+
for (const polygon of preparedPolygons) {
|
|
27
|
+
if (!this.#sourceMap.has(polygon.source)) {
|
|
28
|
+
this.#sourceMap.set(polygon.source, polygon)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns prepared polygons whose bounds overlap requested bounds.
|
|
35
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
|
|
36
|
+
* @param {{ epsilon?: number, stable?: boolean }} [options]
|
|
37
|
+
* @returns {*[]}
|
|
38
|
+
*/
|
|
39
|
+
query(bounds, options = {}) {
|
|
40
|
+
return this.#index.query(bounds, options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the earliest prepared polygon for one source identity.
|
|
45
|
+
* @param {*} source
|
|
46
|
+
* @returns {* | null}
|
|
47
|
+
*/
|
|
48
|
+
resolveSource(source) {
|
|
49
|
+
return this.#sourceMap.get(source) ?? null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves prepared polygon bounds for the top-level AABB index.
|
|
54
|
+
* @param {{ bounds: { minX: number, maxX: number, minY: number, maxY: number } }} polygon
|
|
55
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
|
|
56
|
+
*/
|
|
57
|
+
static #resolveBounds(polygon) {
|
|
58
|
+
return polygon.bounds
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolves stable order from a polygon's position in this specific set.
|
|
63
|
+
* @param {*} _polygon
|
|
64
|
+
* @param {number} index
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
static #resolveSetPosition(_polygon, index) {
|
|
68
|
+
return index
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -8,6 +8,7 @@ import { PcbScene3dCompanionBasePlacementAdjuster } from './PcbScene3dCompanionB
|
|
|
8
8
|
import { PcbScene3dCopperDetailGroupBuilder } from './PcbScene3dCopperDetailGroupBuilder.mjs'
|
|
9
9
|
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
10
10
|
import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
|
|
11
|
+
import { PcbScene3dDeferredModelFinalizer } from './PcbScene3dDeferredModelFinalizer.mjs'
|
|
11
12
|
import { PcbScene3dDrillVoidFactory } from './PcbScene3dDrillVoidFactory.mjs'
|
|
12
13
|
import { PcbScene3dExternalCompanionFallback } from './PcbScene3dExternalCompanionFallback.mjs'
|
|
13
14
|
import { PcbScene3dExternalModels } from './PcbScene3dExternalModels.mjs'
|
|
@@ -76,7 +77,7 @@ export class PcbScene3dRuntime {
|
|
|
76
77
|
/**
|
|
77
78
|
* @param {HTMLElement} viewportNode
|
|
78
79
|
* @param {any} sceneDescription Scene description or CircuitJSON model.
|
|
79
|
-
* @param {{ setDiagnostics?: (messages: string[]) => void, setSelection?: (selection: any | null) => void, loadRuntimeModules?: () => Promise<{ THREE: any, OrbitControls: any }>, translate?: ((key: string) => string) | null }} [hooks]
|
|
80
|
+
* @param {{ setDiagnostics?: (messages: string[]) => void, setSelection?: (selection: any | null) => void, loadRuntimeModules?: () => Promise<{ THREE: any, OrbitControls: any }>, translate?: ((key: string) => string) | null, modelLoaderOptions?: object }} [hooks]
|
|
80
81
|
*/
|
|
81
82
|
constructor(viewportNode, sceneDescription, hooks = {}) {
|
|
82
83
|
const renderModel =
|
|
@@ -264,7 +265,6 @@ export class PcbScene3dRuntime {
|
|
|
264
265
|
this.#settleReady()
|
|
265
266
|
return
|
|
266
267
|
}
|
|
267
|
-
|
|
268
268
|
try {
|
|
269
269
|
const { THREE, OrbitControls } =
|
|
270
270
|
(await this.#hooks.loadRuntimeModules?.()) ||
|
|
@@ -274,7 +274,6 @@ export class PcbScene3dRuntime {
|
|
|
274
274
|
if (this.#isDisposed || !this.#viewportNode) {
|
|
275
275
|
return
|
|
276
276
|
}
|
|
277
|
-
|
|
278
277
|
this.#createRenderer()
|
|
279
278
|
this.#createSceneGraph()
|
|
280
279
|
this.#createControls()
|
|
@@ -291,7 +290,6 @@ export class PcbScene3dRuntime {
|
|
|
291
290
|
this.#settleReady()
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
|
-
|
|
295
293
|
/**
|
|
296
294
|
* Creates the Three.js renderer, camera, and scene.
|
|
297
295
|
* @returns {void}
|
|
@@ -299,7 +297,6 @@ export class PcbScene3dRuntime {
|
|
|
299
297
|
#createRenderer() {
|
|
300
298
|
const THREE = this.#three
|
|
301
299
|
const size = PcbScene3dViewportResize.resolveSize(this.#viewportNode)
|
|
302
|
-
|
|
303
300
|
this.#renderer = new THREE.WebGLRenderer({
|
|
304
301
|
antialias: true,
|
|
305
302
|
alpha: true,
|
|
@@ -310,7 +307,6 @@ export class PcbScene3dRuntime {
|
|
|
310
307
|
this.#renderer.domElement.className = 'scene-3d__canvas'
|
|
311
308
|
this.#renderer.domElement.style.width = '100%'
|
|
312
309
|
this.#renderer.domElement.style.height = '100%'
|
|
313
|
-
|
|
314
310
|
this.#scene = new THREE.Scene()
|
|
315
311
|
this.#camera = new THREE.PerspectiveCamera(
|
|
316
312
|
38,
|
|
@@ -321,7 +317,6 @@ export class PcbScene3dRuntime {
|
|
|
321
317
|
this.#camera.up.set(0, 0, 1)
|
|
322
318
|
this.#viewportNode?.replaceChildren(this.#renderer.domElement)
|
|
323
319
|
}
|
|
324
|
-
|
|
325
320
|
/**
|
|
326
321
|
* Creates the initial board shell, fallback bodies, and placeholder detail
|
|
327
322
|
* groups used by the deferred loading stages.
|
|
@@ -330,7 +325,6 @@ export class PcbScene3dRuntime {
|
|
|
330
325
|
#createSceneGraph() {
|
|
331
326
|
const THREE = this.#three
|
|
332
327
|
const board = this.#sceneDescription.board
|
|
333
|
-
|
|
334
328
|
this.#viewOrientationGroup = new THREE.Group()
|
|
335
329
|
this.#scene.add(this.#viewOrientationGroup)
|
|
336
330
|
this.#rootGroup = new THREE.Group()
|
|
@@ -368,7 +362,11 @@ export class PcbScene3dRuntime {
|
|
|
368
362
|
(x, y) => this.#normalizeDetailPoint(x, y),
|
|
369
363
|
{
|
|
370
364
|
enabled: true,
|
|
371
|
-
board
|
|
365
|
+
board,
|
|
366
|
+
hasBoardAssemblyModel: Boolean(
|
|
367
|
+
this.#sceneDescription.boardAssemblyModel
|
|
368
|
+
),
|
|
369
|
+
sourceFormat: this.#sceneDescription.sourceFormat
|
|
372
370
|
}
|
|
373
371
|
)
|
|
374
372
|
)
|
|
@@ -400,7 +398,6 @@ export class PcbScene3dRuntime {
|
|
|
400
398
|
if (component?.renderFallbackBody === false) {
|
|
401
399
|
return
|
|
402
400
|
}
|
|
403
|
-
|
|
404
401
|
const isCompanionBase =
|
|
405
402
|
PcbScene3dExternalCompanionFallback.shouldKeepFallback(
|
|
406
403
|
this.#sceneDescription,
|
|
@@ -470,7 +467,6 @@ export class PcbScene3dRuntime {
|
|
|
470
467
|
boardSpan * 7
|
|
471
468
|
)
|
|
472
469
|
}
|
|
473
|
-
|
|
474
470
|
/**
|
|
475
471
|
* Applies the active preset's scene-scale transform.
|
|
476
472
|
* @param {string} preset
|
|
@@ -496,27 +492,47 @@ export class PcbScene3dRuntime {
|
|
|
496
492
|
)
|
|
497
493
|
)
|
|
498
494
|
}
|
|
499
|
-
|
|
500
495
|
/**
|
|
501
496
|
* Loads silkscreen, copper, and external model detail after the initial
|
|
502
497
|
* shell render and keeps readiness pending until settlement completes.
|
|
503
498
|
* @returns {Promise<void>}
|
|
504
499
|
*/
|
|
505
500
|
async #loadDeferredDetail() {
|
|
501
|
+
let externalModels = null
|
|
502
|
+
let hasDeferredStageFailure = false
|
|
506
503
|
try {
|
|
507
504
|
await PcbScene3dRuntimeHelpers.yieldToNextFrame(globalThis)
|
|
508
505
|
if (this.#isDisposed) {
|
|
509
506
|
return
|
|
510
507
|
}
|
|
511
|
-
|
|
508
|
+
externalModels = new PcbScene3dDeferredModelFinalizer(
|
|
509
|
+
this.#loadExternalModels(),
|
|
510
|
+
{
|
|
511
|
+
isDisposed: () => this.#isDisposed,
|
|
512
|
+
onSuccess: (diagnostics) => {
|
|
513
|
+
if (!hasDeferredStageFailure && diagnostics.length) {
|
|
514
|
+
this.#hooks.setDiagnostics?.(diagnostics)
|
|
515
|
+
}
|
|
516
|
+
this.#applyViewScale(this.#presetState.get())
|
|
517
|
+
this.#render()
|
|
518
|
+
},
|
|
519
|
+
onError: (error) => {
|
|
520
|
+
this.#hooks.setDiagnostics?.([
|
|
521
|
+
'Deferred 3D detail could not finish loading: ' +
|
|
522
|
+
String(
|
|
523
|
+
error?.message || error || 'Unknown error.'
|
|
524
|
+
)
|
|
525
|
+
])
|
|
526
|
+
this.#render()
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
)
|
|
512
530
|
await this.#loadDeferredSurfaceArtwork()
|
|
513
531
|
this.#render()
|
|
514
|
-
|
|
515
532
|
await PcbScene3dRuntimeHelpers.yieldToNextFrame(globalThis)
|
|
516
533
|
if (this.#isDisposed) {
|
|
517
534
|
return
|
|
518
535
|
}
|
|
519
|
-
|
|
520
536
|
this.#loadDeferredCopper()
|
|
521
537
|
this.#applyToggleVisibility()
|
|
522
538
|
this.#render()
|
|
@@ -525,18 +541,13 @@ export class PcbScene3dRuntime {
|
|
|
525
541
|
if (this.#isDisposed) {
|
|
526
542
|
return
|
|
527
543
|
}
|
|
528
|
-
|
|
529
|
-
await this.#loadExternalModels()
|
|
530
|
-
if (this.#isDisposed) {
|
|
531
|
-
return
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
this.#render()
|
|
544
|
+
await externalModels.release()
|
|
535
545
|
} catch (error) {
|
|
536
546
|
if (this.#isDisposed) {
|
|
537
547
|
return
|
|
538
548
|
}
|
|
539
|
-
|
|
549
|
+
hasDeferredStageFailure = true
|
|
550
|
+
externalModels?.release({ reportError: false }).catch(() => {})
|
|
540
551
|
this.#hooks.setDiagnostics?.([
|
|
541
552
|
'Deferred 3D detail could not finish loading: ' +
|
|
542
553
|
String(error?.message || error || 'Unknown error.')
|
|
@@ -544,7 +555,6 @@ export class PcbScene3dRuntime {
|
|
|
544
555
|
this.#render()
|
|
545
556
|
}
|
|
546
557
|
}
|
|
547
|
-
|
|
548
558
|
/**
|
|
549
559
|
* Builds and attaches surface artwork detail once after the first frame.
|
|
550
560
|
* @returns {Promise<void>}
|
|
@@ -557,7 +567,6 @@ export class PcbScene3dRuntime {
|
|
|
557
567
|
['silkscreen', silkscreen, Z_MIL.silk, true],
|
|
558
568
|
['paste', this.#sceneDescription.detail.paste, Z_MIL.paste, false]
|
|
559
569
|
]
|
|
560
|
-
|
|
561
570
|
for (const [groupName, artwork, z, prepareFonts] of overlays) {
|
|
562
571
|
if (artwork == null) {
|
|
563
572
|
continue
|
|
@@ -578,7 +587,6 @@ export class PcbScene3dRuntime {
|
|
|
578
587
|
? await loaded
|
|
579
588
|
: loaded) || didLoad
|
|
580
589
|
}
|
|
581
|
-
|
|
582
590
|
if (didLoad) {
|
|
583
591
|
this.#applyViewScale(this.#presetState.get())
|
|
584
592
|
}
|
|
@@ -600,33 +608,39 @@ export class PcbScene3dRuntime {
|
|
|
600
608
|
topZ,
|
|
601
609
|
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
602
610
|
)
|
|
603
|
-
|
|
604
611
|
if (detailGroup.children.length) {
|
|
605
612
|
copperGroup.add(detailGroup)
|
|
606
613
|
this.#applyViewScale(this.#presetState.get())
|
|
607
614
|
}
|
|
608
615
|
}
|
|
609
|
-
|
|
610
616
|
/**
|
|
611
617
|
* Attempts to load any resolved external 3D models.
|
|
612
|
-
* @returns {Promise<
|
|
618
|
+
* @returns {Promise<string[]>}
|
|
613
619
|
*/
|
|
614
620
|
async #loadExternalModels() {
|
|
615
621
|
const externalModelsGroup = this.#groups.get('external-models')
|
|
616
622
|
if (!externalModelsGroup) {
|
|
617
|
-
return
|
|
623
|
+
return []
|
|
618
624
|
}
|
|
619
625
|
|
|
620
626
|
const diagnostics = await PcbScene3dExternalModels.loadIntoScene({
|
|
621
627
|
three: this.#three,
|
|
622
628
|
sceneDescription: this.#placementSceneDescription,
|
|
623
629
|
externalModelsGroup,
|
|
630
|
+
modelLoaderOptions: this.#hooks.modelLoaderOptions,
|
|
624
631
|
modelViewScale: PcbScene3dRuntime.resolveViewScale(
|
|
625
632
|
this.#presetState.get(),
|
|
626
633
|
this.#sceneDescription
|
|
627
634
|
),
|
|
628
635
|
isDisposed: () => this.#isDisposed,
|
|
629
636
|
onPlacementGroup: (placement, placementGroup) => {
|
|
637
|
+
PcbScene3dExternalModels.applyViewCompensation(
|
|
638
|
+
placementGroup,
|
|
639
|
+
PcbScene3dRuntime.resolveViewScale(
|
|
640
|
+
this.#presetState.get(),
|
|
641
|
+
this.#sceneDescription
|
|
642
|
+
)
|
|
643
|
+
)
|
|
630
644
|
this.#registerSelectionRoot(
|
|
631
645
|
placement?.designator,
|
|
632
646
|
placementGroup
|
|
@@ -658,12 +672,8 @@ export class PcbScene3dRuntime {
|
|
|
658
672
|
this.#applyToggleVisibility()
|
|
659
673
|
}
|
|
660
674
|
})
|
|
661
|
-
|
|
662
|
-
if (diagnostics.length) {
|
|
663
|
-
this.#hooks.setDiagnostics?.(diagnostics)
|
|
664
|
-
}
|
|
675
|
+
return diagnostics
|
|
665
676
|
}
|
|
666
|
-
|
|
667
677
|
/**
|
|
668
678
|
* Creates and configures orbit/pan/zoom controls using Three's standard
|
|
669
679
|
* OrbitControls implementation in the same z-up world as the PCB scene.
|
|
@@ -673,13 +683,11 @@ export class PcbScene3dRuntime {
|
|
|
673
683
|
if (!this.#camera || !this.#renderer || !this.#orbitControlsClass) {
|
|
674
684
|
return
|
|
675
685
|
}
|
|
676
|
-
|
|
677
686
|
const THREE = this.#three
|
|
678
687
|
const domElement = this.#renderer?.domElement
|
|
679
688
|
if (!domElement) {
|
|
680
689
|
return
|
|
681
690
|
}
|
|
682
|
-
|
|
683
691
|
this.#controls = new this.#orbitControlsClass(this.#camera, domElement)
|
|
684
692
|
this.#controls.enableDamping = false
|
|
685
693
|
this.#controls.screenSpacePanning = true
|
|
@@ -691,14 +699,12 @@ export class PcbScene3dRuntime {
|
|
|
691
699
|
THREE,
|
|
692
700
|
this.#presetState.get()
|
|
693
701
|
)
|
|
694
|
-
|
|
695
702
|
PcbScene3dCameraRig.applyPreset(
|
|
696
703
|
this.#camera,
|
|
697
704
|
this.#controls,
|
|
698
705
|
this.#presetState.get(),
|
|
699
706
|
this.#sceneDescription
|
|
700
707
|
)
|
|
701
|
-
|
|
702
708
|
this.#bindListener(this.#controls, 'change', () => {
|
|
703
709
|
this.#renderScheduler.schedule()
|
|
704
710
|
})
|
|
@@ -712,22 +718,18 @@ export class PcbScene3dRuntime {
|
|
|
712
718
|
() => this.#handleResize()
|
|
713
719
|
)
|
|
714
720
|
}
|
|
715
|
-
|
|
716
721
|
/** @returns {void} */
|
|
717
722
|
#bindSelectionInteraction() {
|
|
718
723
|
if (!this.#renderer || !this.#camera || !this.#three) {
|
|
719
724
|
return
|
|
720
725
|
}
|
|
721
|
-
|
|
722
726
|
this.#raycaster = new this.#three.Raycaster()
|
|
723
727
|
this.#pointer = new this.#three.Vector2()
|
|
724
728
|
const domElement = this.#renderer.domElement
|
|
725
|
-
|
|
726
729
|
this.#bindListener(domElement, 'pointerdown', (event) => {
|
|
727
730
|
if (Number(event?.button) !== 0) {
|
|
728
731
|
return
|
|
729
732
|
}
|
|
730
|
-
|
|
731
733
|
this.#pointerDownPosition = {
|
|
732
734
|
x: Number(event?.clientX || 0),
|
|
733
735
|
y: Number(event?.clientY || 0)
|
|
@@ -737,7 +739,6 @@ export class PcbScene3dRuntime {
|
|
|
737
739
|
if (Number(event?.button) !== 0) {
|
|
738
740
|
return
|
|
739
741
|
}
|
|
740
|
-
|
|
741
742
|
if (
|
|
742
743
|
!this.#pointerDownPosition ||
|
|
743
744
|
PcbScene3dRuntimeHelpers.pointerTravel(
|
|
@@ -751,7 +752,6 @@ export class PcbScene3dRuntime {
|
|
|
751
752
|
this.#pointerDownPosition = null
|
|
752
753
|
return
|
|
753
754
|
}
|
|
754
|
-
|
|
755
755
|
this.#pointerDownPosition = null
|
|
756
756
|
this.#handleSelectionPointer(event)
|
|
757
757
|
})
|
|
@@ -68,6 +68,39 @@ export class PcbScene3dRuntimeBoardMeshes {
|
|
|
68
68
|
* @returns {any}
|
|
69
69
|
*/
|
|
70
70
|
static buildBoardMesh(THREE, sceneDescription, normalizeDetailPoint) {
|
|
71
|
+
const contours = PcbScene3dRuntimeBoardMeshes.#boardContours(
|
|
72
|
+
sceneDescription.board
|
|
73
|
+
)
|
|
74
|
+
if (contours.length === 1) {
|
|
75
|
+
return PcbScene3dRuntimeBoardMeshes.#buildBoardMesh(
|
|
76
|
+
THREE,
|
|
77
|
+
{ ...sceneDescription, board: contours[0] },
|
|
78
|
+
normalizeDetailPoint
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const group = new THREE.Group()
|
|
83
|
+
group.name = 'board-contours'
|
|
84
|
+
contours.forEach((board, index) => {
|
|
85
|
+
const mesh = PcbScene3dRuntimeBoardMeshes.#buildBoardMesh(
|
|
86
|
+
THREE,
|
|
87
|
+
{ ...sceneDescription, board },
|
|
88
|
+
normalizeDetailPoint
|
|
89
|
+
)
|
|
90
|
+
mesh.name = 'board-contour-' + (index + 1)
|
|
91
|
+
group.add(mesh)
|
|
92
|
+
})
|
|
93
|
+
return group
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds one independently extruded board contour.
|
|
98
|
+
* @param {any} THREE Three.js namespace.
|
|
99
|
+
* @param {{ board?: any, detail?: any, boardAssemblyModel?: any }} sceneDescription Scene metadata.
|
|
100
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeDetailPoint Detail coordinate normalizer.
|
|
101
|
+
* @returns {any}
|
|
102
|
+
*/
|
|
103
|
+
static #buildBoardMesh(THREE, sceneDescription, normalizeDetailPoint) {
|
|
71
104
|
const board = sceneDescription.board
|
|
72
105
|
const geometry = PcbScene3dBoardShapeFactory.buildGeometry(
|
|
73
106
|
THREE,
|
|
@@ -97,7 +130,7 @@ export class PcbScene3dRuntimeBoardMeshes {
|
|
|
97
130
|
: 0xc9ca78
|
|
98
131
|
const surfaceColor = hasBoardAssemblyModel
|
|
99
132
|
? resolvedEdgeColor
|
|
100
|
-
: PcbScene3dBoardMaterialPalette.
|
|
133
|
+
: PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {
|
|
101
134
|
hasBoardAssemblyModel
|
|
102
135
|
})
|
|
103
136
|
|
|
@@ -140,6 +173,39 @@ export class PcbScene3dRuntimeBoardMeshes {
|
|
|
140
173
|
* @returns {any}
|
|
141
174
|
*/
|
|
142
175
|
static buildBoardOutline(THREE, sceneDescription, normalizeDetailPoint) {
|
|
176
|
+
const contours = PcbScene3dRuntimeBoardMeshes.#boardContours(
|
|
177
|
+
sceneDescription.board
|
|
178
|
+
)
|
|
179
|
+
if (contours.length === 1) {
|
|
180
|
+
return PcbScene3dRuntimeBoardMeshes.#buildBoardOutline(
|
|
181
|
+
THREE,
|
|
182
|
+
{ ...sceneDescription, board: contours[0] },
|
|
183
|
+
normalizeDetailPoint
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const group = new THREE.Group()
|
|
188
|
+
group.name = 'board-contour-outlines'
|
|
189
|
+
contours.forEach((board, index) => {
|
|
190
|
+
const outline = PcbScene3dRuntimeBoardMeshes.#buildBoardOutline(
|
|
191
|
+
THREE,
|
|
192
|
+
{ ...sceneDescription, board },
|
|
193
|
+
normalizeDetailPoint
|
|
194
|
+
)
|
|
195
|
+
outline.name = 'board-contour-outline-' + (index + 1)
|
|
196
|
+
group.add(outline)
|
|
197
|
+
})
|
|
198
|
+
return group
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds one independently rendered board contour outline.
|
|
203
|
+
* @param {any} THREE Three.js namespace.
|
|
204
|
+
* @param {{ board?: any, detail?: any }} sceneDescription Scene metadata.
|
|
205
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeDetailPoint Detail coordinate normalizer.
|
|
206
|
+
* @returns {any}
|
|
207
|
+
*/
|
|
208
|
+
static #buildBoardOutline(THREE, sceneDescription, normalizeDetailPoint) {
|
|
143
209
|
const shape = PcbScene3dBoardShapeFactory.buildShape(
|
|
144
210
|
THREE,
|
|
145
211
|
sceneDescription.board,
|
|
@@ -166,6 +232,24 @@ export class PcbScene3dRuntimeBoardMeshes {
|
|
|
166
232
|
)
|
|
167
233
|
}
|
|
168
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Resolves independently renderable contours while preserving legacy boards.
|
|
237
|
+
* @param {object} board Aggregate board metadata.
|
|
238
|
+
* @returns {object[]}
|
|
239
|
+
*/
|
|
240
|
+
static #boardContours(board) {
|
|
241
|
+
const contours = Array.isArray(board?.contours)
|
|
242
|
+
? board.contours.filter(Boolean)
|
|
243
|
+
: []
|
|
244
|
+
if (!contours.length) return [board || {}]
|
|
245
|
+
|
|
246
|
+
return contours.map((contour) => ({
|
|
247
|
+
...board,
|
|
248
|
+
...contour,
|
|
249
|
+
contours: []
|
|
250
|
+
}))
|
|
251
|
+
}
|
|
252
|
+
|
|
169
253
|
/**
|
|
170
254
|
* Traverses the board render group.
|
|
171
255
|
* @param {any} root Root group or mesh.
|
|
@@ -12,9 +12,10 @@ export class PcbScene3dShapeHoleGeometryCleaner {
|
|
|
12
12
|
* @param {any} THREE Three.js namespace.
|
|
13
13
|
* @param {any} geometry Shape geometry.
|
|
14
14
|
* @param {{ x: number, y: number }[][]} holes Shape hole polygons.
|
|
15
|
+
* @param {{ preparedPolygonCache?: Map }} [options] Request-scoped options.
|
|
15
16
|
* @returns {any}
|
|
16
17
|
*/
|
|
17
|
-
static removeCoveredHoleCenters(THREE, geometry, holes) {
|
|
18
|
+
static removeCoveredHoleCenters(THREE, geometry, holes, options = {}) {
|
|
18
19
|
const coveredHoles =
|
|
19
20
|
PcbScene3dShapeHoleGeometryCleaner.resolveCoveredHoleCenters(
|
|
20
21
|
geometry,
|
|
@@ -32,7 +33,8 @@ export class PcbScene3dShapeHoleGeometryCleaner {
|
|
|
32
33
|
{
|
|
33
34
|
maxDepth: 12,
|
|
34
35
|
maxEdgeLength:
|
|
35
|
-
PcbScene3dShapeHoleGeometryCleaner.#CUTOUT_MAX_EDGE_LENGTH
|
|
36
|
+
PcbScene3dShapeHoleGeometryCleaner.#CUTOUT_MAX_EDGE_LENGTH,
|
|
37
|
+
preparedPolygonCache: options?.preparedPolygonCache
|
|
36
38
|
}
|
|
37
39
|
)
|
|
38
40
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from 'circuitjson-toolkit'
|
|
2
|
+
import { CircuitJsonBomBuilder } from 'circuitjson-toolkit/extensions'
|
|
3
|
+
import { PcbScene3dCircuitJsonGeometry } from './PcbScene3dCircuitJsonGeometry.mjs'
|
|
4
|
+
import { PcbScene3dCircuitJsonInput } from './PcbScene3dCircuitJsonInput.mjs'
|
|
1
5
|
import { PcbScene3dText } from './PcbScene3dText.mjs'
|
|
2
6
|
|
|
3
7
|
/**
|
|
@@ -13,8 +17,8 @@ export class PcbScene3dShellRenderer {
|
|
|
13
17
|
*/
|
|
14
18
|
static render(documentModel, translate = null, options = {}) {
|
|
15
19
|
const t = PcbScene3dText.createTranslator(translate)
|
|
16
|
-
const
|
|
17
|
-
if (!
|
|
20
|
+
const summary = PcbScene3dShellRenderer.#summary(documentModel, options)
|
|
21
|
+
if (!summary) {
|
|
18
22
|
return (
|
|
19
23
|
'<section class="viewer-empty">' +
|
|
20
24
|
PcbScene3dShellRenderer.#escapeHtml(t('scene3d.noPcb')) +
|
|
@@ -22,10 +26,10 @@ export class PcbScene3dShellRenderer {
|
|
|
22
26
|
)
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
const widthMil = Math.round(
|
|
26
|
-
const heightMil = Math.round(
|
|
27
|
-
const componentCount =
|
|
28
|
-
const bomRows =
|
|
29
|
+
const widthMil = Math.round(summary.widthMil || 0)
|
|
30
|
+
const heightMil = Math.round(summary.heightMil || 0)
|
|
31
|
+
const componentCount = summary.componentCount
|
|
32
|
+
const bomRows = summary.bomRows
|
|
29
33
|
|
|
30
34
|
return (
|
|
31
35
|
'<section class="scene-3d"><header class="svg-panel__header"><h3>' +
|
|
@@ -129,6 +133,71 @@ export class PcbScene3dShellRenderer {
|
|
|
129
133
|
)
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Resolves shell statistics from legacy or canonical CircuitJSON input.
|
|
138
|
+
* @param {unknown} documentModel Document input.
|
|
139
|
+
* @param {{ drawFauxBoard?: boolean }} options Shell and adapter options.
|
|
140
|
+
* @returns {{ widthMil: number, heightMil: number, componentCount: number, bomRows: number } | null}
|
|
141
|
+
*/
|
|
142
|
+
static #summary(documentModel, options) {
|
|
143
|
+
const legacyPcb = PcbScene3dShellRenderer.#ownData(documentModel, 'pcb')
|
|
144
|
+
if (legacyPcb && typeof legacyPcb === 'object') {
|
|
145
|
+
const boardOutline = legacyPcb.boardOutline || {}
|
|
146
|
+
const components = Array.isArray(legacyPcb.components)
|
|
147
|
+
? legacyPcb.components
|
|
148
|
+
: []
|
|
149
|
+
const bom = PcbScene3dShellRenderer.#ownData(documentModel, 'bom')
|
|
150
|
+
return {
|
|
151
|
+
widthMil: Number(boardOutline.widthMil || 0),
|
|
152
|
+
heightMil: Number(boardOutline.heightMil || 0),
|
|
153
|
+
componentCount: components.length,
|
|
154
|
+
bomRows: Array.isArray(bom) ? bom.length : 0
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!PcbScene3dCircuitJsonInput.isModel(documentModel)) return null
|
|
158
|
+
|
|
159
|
+
const context = CircuitJsonDocumentContext.prepare(documentModel, {
|
|
160
|
+
indexes: ['elements']
|
|
161
|
+
})
|
|
162
|
+
const index = context.getIndex('elements')
|
|
163
|
+
if (
|
|
164
|
+
!index.elementsByType.get('pcb_board')?.length &&
|
|
165
|
+
!index.elementsByType.get('pcb_panel')?.length &&
|
|
166
|
+
!(
|
|
167
|
+
options?.drawFauxBoard === true &&
|
|
168
|
+
index.elementsByType.get('pcb_component')?.length
|
|
169
|
+
)
|
|
170
|
+
) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
const board = PcbScene3dCircuitJsonGeometry.buildBoard(index, options)
|
|
174
|
+
return {
|
|
175
|
+
widthMil: board.widthMil,
|
|
176
|
+
heightMil: board.heightMil,
|
|
177
|
+
componentCount: (index.elementsByType.get('pcb_component') || [])
|
|
178
|
+
.length,
|
|
179
|
+
bomRows: CircuitJsonBomBuilder.build(context.model).length
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reads one own data property without invoking accessors.
|
|
185
|
+
* @param {unknown} value Record candidate.
|
|
186
|
+
* @param {PropertyKey} key Property key.
|
|
187
|
+
* @returns {unknown}
|
|
188
|
+
*/
|
|
189
|
+
static #ownData(value, key) {
|
|
190
|
+
if (!value || typeof value !== 'object') return undefined
|
|
191
|
+
try {
|
|
192
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
193
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
194
|
+
? descriptor.value
|
|
195
|
+
: undefined
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
132
201
|
/**
|
|
133
202
|
* Returns the checkbox checked attribute for one scene toggle.
|
|
134
203
|
* @param {{ initialToggles?: Record<string, boolean> }} options Renderer options.
|