pcb-scene3d-viewer 1.0.1 → 1.1.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/package.json +1 -1
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +14 -2
- package/src/PcbScene3dBoardAssemblyTransform.mjs +180 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +123 -10
- package/src/PcbScene3dBoardShapeFactory.mjs +95 -5
- package/src/PcbScene3dBodyColor.mjs +26 -0
- package/src/PcbScene3dBufferAttributeFactory.mjs +128 -0
- package/src/PcbScene3dCutoutCircleDetector.mjs +84 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +142 -32
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +3 -30
- package/src/PcbScene3dExternalModels.mjs +30 -15
- package/src/PcbScene3dRuntime.mjs +24 -33
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +80 -1
- package/src/PcbScene3dSilkscreenChunkedFactory.mjs +217 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +49 -8
- package/src/PcbScene3dStepLoader.mjs +100 -27
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects sampled circular drill cutouts for faster geometry clipping.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dCutoutCircleDetector {
|
|
5
|
+
static #DEFAULT_EPSILON = 0.001
|
|
6
|
+
static #MAX_RELATIVE_ERROR = 0.025
|
|
7
|
+
static #MIN_POINT_COUNT = 8
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves circle metadata for sampled circular drill cutouts.
|
|
11
|
+
* @param {{ x: number, y: number }[]} points
|
|
12
|
+
* @param {number} [epsilon]
|
|
13
|
+
* @returns {{ isCircular: true, centerX: number, centerY: number, radius: number } | null}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(points, epsilon = this.#DEFAULT_EPSILON) {
|
|
16
|
+
if (
|
|
17
|
+
!Array.isArray(points) ||
|
|
18
|
+
points.length < PcbScene3dCutoutCircleDetector.#MIN_POINT_COUNT
|
|
19
|
+
) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const center = PcbScene3dCutoutCircleDetector.#resolveCentroid(points)
|
|
24
|
+
const radii = points.map((point) =>
|
|
25
|
+
Math.hypot(point.x - center.x, point.y - center.y)
|
|
26
|
+
)
|
|
27
|
+
const radius =
|
|
28
|
+
radii.reduce((sum, value) => sum + value, 0) / radii.length
|
|
29
|
+
const maxError = Math.max(
|
|
30
|
+
...radii.map((value) => Math.abs(value - radius))
|
|
31
|
+
)
|
|
32
|
+
const tolerance = Math.max(
|
|
33
|
+
Number(epsilon || 0),
|
|
34
|
+
radius * PcbScene3dCutoutCircleDetector.#MAX_RELATIVE_ERROR
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!Number.isFinite(radius) ||
|
|
39
|
+
radius <= Number(epsilon || 0) ||
|
|
40
|
+
maxError > tolerance
|
|
41
|
+
) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
isCircular: true,
|
|
47
|
+
centerX: center.x,
|
|
48
|
+
centerY: center.y,
|
|
49
|
+
radius
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves squared distance from a point to circular cutout center.
|
|
55
|
+
* @param {{ x: number, y: number }} point
|
|
56
|
+
* @param {{ centerX?: number, centerY?: number }} cutout
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
static distanceSquared(point, cutout) {
|
|
60
|
+
const dx = point.x - Number(cutout.centerX || 0)
|
|
61
|
+
const dy = point.y - Number(cutout.centerY || 0)
|
|
62
|
+
return dx * dx + dy * dy
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the centroid of one point list.
|
|
67
|
+
* @param {{ x: number, y: number }[]} points
|
|
68
|
+
* @returns {{ x: number, y: number }}
|
|
69
|
+
*/
|
|
70
|
+
static #resolveCentroid(points) {
|
|
71
|
+
const sum = points.reduce(
|
|
72
|
+
(accumulator, point) => ({
|
|
73
|
+
x: accumulator.x + Number(point.x || 0),
|
|
74
|
+
y: accumulator.y + Number(point.y || 0)
|
|
75
|
+
}),
|
|
76
|
+
{ x: 0, y: 0 }
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
x: sum.x / points.length,
|
|
81
|
+
y: sum.y / points.length
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Clips filled 2D geometry against drill-cutout polygons.
|
|
3
5
|
*/
|
|
@@ -79,7 +81,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
79
81
|
/**
|
|
80
82
|
* Resolves clipping settings.
|
|
81
83
|
* @param {{ maxDepth?: number, maxEdgeLength?: number }} options
|
|
82
|
-
* @returns {{ maxDepth: number, maxEdgeLength: number }}
|
|
84
|
+
* @returns {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number }}
|
|
83
85
|
*/
|
|
84
86
|
static #resolveSettings(options) {
|
|
85
87
|
const maxEdgeLength = Math.max(
|
|
@@ -94,24 +96,35 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
94
96
|
PcbScene3dCutoutGeometryFilter.#DEFAULT_MAX_DEPTH,
|
|
95
97
|
0
|
|
96
98
|
),
|
|
97
|
-
maxEdgeLength
|
|
99
|
+
maxEdgeLength,
|
|
100
|
+
maxEdgeLengthSquared: maxEdgeLength * maxEdgeLength
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
/**
|
|
102
105
|
* Prepares cutout polygons with bounds for fast overlap checks.
|
|
103
106
|
* @param {{ x: number, y: number }[][]} cutouts
|
|
104
|
-
* @returns {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]}
|
|
107
|
+
* @returns {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }[]}
|
|
105
108
|
*/
|
|
106
109
|
static #prepareCutouts(cutouts) {
|
|
107
110
|
return cutouts
|
|
108
111
|
.filter((cutout) => Array.isArray(cutout) && cutout.length >= 3)
|
|
109
|
-
.map((cutout) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
.map((cutout) => {
|
|
113
|
+
const circularCutout =
|
|
114
|
+
PcbScene3dCutoutCircleDetector.resolve(cutout)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
points: cutout,
|
|
118
|
+
segments:
|
|
119
|
+
PcbScene3dCutoutGeometryFilter.#buildCutoutSegments(
|
|
120
|
+
cutout
|
|
121
|
+
),
|
|
122
|
+
bounds: PcbScene3dCutoutGeometryFilter.#resolveBounds(
|
|
123
|
+
cutout
|
|
124
|
+
),
|
|
125
|
+
...circularCutout
|
|
126
|
+
}
|
|
127
|
+
})
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
/**
|
|
@@ -151,7 +164,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
151
164
|
* @param {number[]} positions
|
|
152
165
|
* @param {{ x: number, y: number, z: number }[]} triangle
|
|
153
166
|
* @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} cutouts
|
|
154
|
-
* @param {{ maxDepth: number, maxEdgeLength: number }} settings
|
|
167
|
+
* @param {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number }} settings
|
|
155
168
|
* @param {number} depth
|
|
156
169
|
* @param {{ changed: boolean }} state
|
|
157
170
|
* @param {object | null} [cutoutIndex]
|
|
@@ -199,8 +212,8 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
199
212
|
|
|
200
213
|
if (
|
|
201
214
|
depth >= settings.maxDepth ||
|
|
202
|
-
PcbScene3dCutoutGeometryFilter.#
|
|
203
|
-
settings.
|
|
215
|
+
PcbScene3dCutoutGeometryFilter.#maxEdgeLengthSquared(triangle) <=
|
|
216
|
+
settings.maxEdgeLengthSquared
|
|
204
217
|
) {
|
|
205
218
|
return
|
|
206
219
|
}
|
|
@@ -443,23 +456,22 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
443
456
|
}
|
|
444
457
|
|
|
445
458
|
/**
|
|
446
|
-
* Resolves the longest edge in one triangle.
|
|
459
|
+
* Resolves the longest squared edge length in one triangle.
|
|
447
460
|
* @param {{ x: number, y: number }[]} triangle
|
|
448
461
|
* @returns {number}
|
|
449
462
|
*/
|
|
450
|
-
static #
|
|
451
|
-
let
|
|
463
|
+
static #maxEdgeLengthSquared(triangle) {
|
|
464
|
+
let maxLengthSquared = 0
|
|
452
465
|
|
|
453
466
|
for (let index = 0; index < triangle.length; index += 1) {
|
|
454
467
|
const point = triangle[index]
|
|
455
468
|
const next = triangle[(index + 1) % triangle.length]
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
469
|
+
const dx = point.x - next.x
|
|
470
|
+
const dy = point.y - next.y
|
|
471
|
+
maxLengthSquared = Math.max(maxLengthSquared, dx * dx + dy * dy)
|
|
460
472
|
}
|
|
461
473
|
|
|
462
|
-
return
|
|
474
|
+
return maxLengthSquared
|
|
463
475
|
}
|
|
464
476
|
|
|
465
477
|
/**
|
|
@@ -496,10 +508,17 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
496
508
|
*/
|
|
497
509
|
static #boundsOverlap(first, second) {
|
|
498
510
|
return (
|
|
499
|
-
first.minX <=
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
first.
|
|
511
|
+
first.minX <=
|
|
512
|
+
second.maxX +
|
|
513
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
514
|
+
first.maxX >=
|
|
515
|
+
second.minX -
|
|
516
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
517
|
+
first.minY <=
|
|
518
|
+
second.maxY +
|
|
519
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
520
|
+
first.maxY >=
|
|
521
|
+
second.minY - PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
503
522
|
)
|
|
504
523
|
}
|
|
505
524
|
|
|
@@ -543,7 +562,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
543
562
|
/**
|
|
544
563
|
* Returns true when one triangle intersects or covers a cutout.
|
|
545
564
|
* @param {{ x: number, y: number }[]} triangle
|
|
546
|
-
* @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }} cutout
|
|
565
|
+
* @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
547
566
|
* @returns {boolean}
|
|
548
567
|
*/
|
|
549
568
|
static #doesTriangleOverlapCutout(triangle, cutout) {
|
|
@@ -587,10 +606,28 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
587
606
|
/**
|
|
588
607
|
* Returns true when a point is inside or on a cutout.
|
|
589
608
|
* @param {{ x: number, y: number }} point
|
|
590
|
-
* @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }} cutout
|
|
609
|
+
* @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
591
610
|
* @returns {boolean}
|
|
592
611
|
*/
|
|
593
612
|
static #isPointInsideOrOnCutout(point, cutout) {
|
|
613
|
+
if (
|
|
614
|
+
!PcbScene3dCutoutGeometryFilter.#pointOverlapsBounds(
|
|
615
|
+
point,
|
|
616
|
+
cutout.bounds
|
|
617
|
+
)
|
|
618
|
+
) {
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (cutout.isCircular) {
|
|
623
|
+
return (
|
|
624
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <=
|
|
625
|
+
(Number(cutout.radius || 0) +
|
|
626
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON) **
|
|
627
|
+
2
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
594
631
|
return (
|
|
595
632
|
PcbScene3dCutoutGeometryFilter.#isPointOnCutoutBoundary(
|
|
596
633
|
point,
|
|
@@ -603,13 +640,47 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
603
640
|
)
|
|
604
641
|
}
|
|
605
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Returns true when a point lies within a bounding box tolerance.
|
|
645
|
+
* @param {{ x: number, y: number }} point
|
|
646
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
|
|
647
|
+
* @returns {boolean}
|
|
648
|
+
*/
|
|
649
|
+
static #pointOverlapsBounds(point, bounds) {
|
|
650
|
+
return (
|
|
651
|
+
point.x >=
|
|
652
|
+
bounds.minX -
|
|
653
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
654
|
+
point.x <=
|
|
655
|
+
bounds.maxX +
|
|
656
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
657
|
+
point.y >=
|
|
658
|
+
bounds.minY -
|
|
659
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
660
|
+
point.y <=
|
|
661
|
+
bounds.maxY + PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
606
665
|
/**
|
|
607
666
|
* Returns true when a point lies inside a cutout and away from its border.
|
|
608
667
|
* @param {{ x: number, y: number }} point
|
|
609
|
-
* @param {{ points: { x: number, y: number }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }} cutout
|
|
668
|
+
* @param {{ points: { x: number, y: number }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
610
669
|
* @returns {boolean}
|
|
611
670
|
*/
|
|
612
671
|
static #isPointStrictlyInsideCutout(point, cutout) {
|
|
672
|
+
if (cutout.isCircular) {
|
|
673
|
+
const radius = Math.max(
|
|
674
|
+
0,
|
|
675
|
+
Number(cutout.radius || 0) -
|
|
676
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
677
|
+
)
|
|
678
|
+
return (
|
|
679
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <
|
|
680
|
+
radius * radius
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
|
|
613
684
|
const polygon = cutout.points
|
|
614
685
|
|
|
615
686
|
let inside = false
|
|
@@ -638,18 +709,33 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
638
709
|
/**
|
|
639
710
|
* Returns true when a point lies on a cutout edge.
|
|
640
711
|
* @param {{ x: number, y: number }} point
|
|
641
|
-
* @param {{
|
|
712
|
+
* @param {{ segments: { start: { x: number, y: number }, end: { x: number, y: number }, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
642
713
|
* @returns {boolean}
|
|
643
714
|
*/
|
|
644
715
|
static #isPointOnCutoutBoundary(point, cutout) {
|
|
645
|
-
|
|
716
|
+
if (cutout.isCircular) {
|
|
717
|
+
return (
|
|
718
|
+
Math.abs(
|
|
719
|
+
Math.sqrt(
|
|
720
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(
|
|
721
|
+
point,
|
|
722
|
+
cutout
|
|
723
|
+
)
|
|
724
|
+
) - Number(cutout.radius || 0)
|
|
725
|
+
) <= PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
726
|
+
)
|
|
727
|
+
}
|
|
646
728
|
|
|
647
|
-
for (
|
|
729
|
+
for (const segment of cutout.segments) {
|
|
648
730
|
if (
|
|
731
|
+
PcbScene3dCutoutGeometryFilter.#pointOverlapsBounds(
|
|
732
|
+
point,
|
|
733
|
+
segment.bounds
|
|
734
|
+
) &&
|
|
649
735
|
PcbScene3dCutoutGeometryFilter.#isPointOnSegment(
|
|
650
736
|
point,
|
|
651
|
-
|
|
652
|
-
|
|
737
|
+
segment.start,
|
|
738
|
+
segment.end
|
|
653
739
|
)
|
|
654
740
|
) {
|
|
655
741
|
return true
|
|
@@ -703,9 +789,18 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
703
789
|
) {
|
|
704
790
|
const triangleStart = triangle[triangleIndex]
|
|
705
791
|
const triangleEnd = triangle[(triangleIndex + 1) % triangle.length]
|
|
792
|
+
const triangleSegmentBounds =
|
|
793
|
+
PcbScene3dCutoutGeometryFilter.#resolveSegmentBounds(
|
|
794
|
+
triangleStart,
|
|
795
|
+
triangleEnd
|
|
796
|
+
)
|
|
706
797
|
|
|
707
798
|
for (const segment of cutout.segments) {
|
|
708
799
|
if (
|
|
800
|
+
PcbScene3dCutoutGeometryFilter.#boundsOverlap(
|
|
801
|
+
triangleSegmentBounds,
|
|
802
|
+
segment.bounds
|
|
803
|
+
) &&
|
|
709
804
|
PcbScene3dCutoutGeometryFilter.#segmentsIntersect(
|
|
710
805
|
triangleStart,
|
|
711
806
|
triangleEnd,
|
|
@@ -721,6 +816,21 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
721
816
|
return false
|
|
722
817
|
}
|
|
723
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Resolves one finite segment bounding box.
|
|
821
|
+
* @param {{ x: number, y: number }} firstStart
|
|
822
|
+
* @param {{ x: number, y: number }} firstEnd
|
|
823
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
|
|
824
|
+
*/
|
|
825
|
+
static #resolveSegmentBounds(firstStart, firstEnd) {
|
|
826
|
+
return {
|
|
827
|
+
minX: Math.min(firstStart.x, firstEnd.x),
|
|
828
|
+
maxX: Math.max(firstStart.x, firstEnd.x),
|
|
829
|
+
minY: Math.min(firstStart.y, firstEnd.y),
|
|
830
|
+
maxY: Math.max(firstStart.y, firstEnd.y)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
724
834
|
/**
|
|
725
835
|
* Returns true when two finite line segments intersect.
|
|
726
836
|
* @param {{ x: number, y: number }} firstStart
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export class PcbScene3dDetailCoordinateNormalizer {
|
|
5
5
|
/**
|
|
6
6
|
* Creates a reusable normalizer for one scene description.
|
|
7
|
-
* @param {{ board?: { centerX?: number, centerY?: number },
|
|
7
|
+
* @param {{ board?: { centerX?: number, centerY?: number }, coordinateSystem?: string, sourceFormat?: string } | null} sceneDescription Scene metadata.
|
|
8
8
|
* @returns {(x: number, y: number) => { x: number, y: number }}
|
|
9
9
|
*/
|
|
10
10
|
static create(sceneDescription) {
|
|
@@ -18,7 +18,7 @@ export class PcbScene3dDetailCoordinateNormalizer {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Normalizes one source detail coordinate into centered scene space.
|
|
21
|
-
* @param {{ board?: { centerX?: number, centerY?: number },
|
|
21
|
+
* @param {{ board?: { centerX?: number, centerY?: number }, coordinateSystem?: string, sourceFormat?: string } | null} sceneDescription Scene metadata.
|
|
22
22
|
* @param {number} x Source X coordinate.
|
|
23
23
|
* @param {number} y Source Y coordinate.
|
|
24
24
|
* @returns {{ x: number, y: number }}
|
|
@@ -32,34 +32,7 @@ export class PcbScene3dDetailCoordinateNormalizer {
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
x: sourceX - centerX,
|
|
35
|
-
y:
|
|
36
|
-
sceneDescription
|
|
37
|
-
)
|
|
38
|
-
? centerY - sourceY
|
|
39
|
-
: sourceY - centerY
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Checks whether detail primitives are being aligned to an assembly model
|
|
45
|
-
* that still uses the source PCB Y axis.
|
|
46
|
-
* @param {{ boardAssemblyModel?: any, coordinateSystem?: string, sourceFormat?: string } | null} sceneDescription Scene metadata.
|
|
47
|
-
* @returns {boolean}
|
|
48
|
-
*/
|
|
49
|
-
static #usesAssemblyModelPcbY(sceneDescription) {
|
|
50
|
-
if (
|
|
51
|
-
String(sceneDescription?.coordinateSystem || '') === 'kicad-3d-y-up'
|
|
52
|
-
) {
|
|
53
|
-
return false
|
|
35
|
+
y: sourceY - centerY
|
|
54
36
|
}
|
|
55
|
-
|
|
56
|
-
const sourceFormat = String(
|
|
57
|
-
sceneDescription?.sourceFormat || ''
|
|
58
|
-
).toLowerCase()
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
sourceFormat === 'altium' &&
|
|
62
|
-
Boolean(sceneDescription?.boardAssemblyModel)
|
|
63
|
-
)
|
|
64
37
|
}
|
|
65
38
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { PcbScene3dBoardAssemblyPresentation } from './PcbScene3dBoardAssemblyPresentation.mjs'
|
|
2
2
|
import { PcbScene3dBoardAssemblyPlacement } from './PcbScene3dBoardAssemblyPlacement.mjs'
|
|
3
|
+
import { PcbScene3dBoardAssemblyTransform } from './PcbScene3dBoardAssemblyTransform.mjs'
|
|
4
|
+
import { PcbScene3dBufferAttributeFactory } from './PcbScene3dBufferAttributeFactory.mjs'
|
|
3
5
|
import { PcbScene3dExternalModelLoadOrder } from './PcbScene3dExternalModelLoadOrder.mjs'
|
|
4
6
|
import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
|
|
5
7
|
import { PcbScene3dStepLoader } from './PcbScene3dStepLoader.mjs'
|
|
@@ -329,7 +331,7 @@ export class PcbScene3dExternalModels {
|
|
|
329
331
|
/**
|
|
330
332
|
* Wraps a full board assembly model in board-local scene coordinates.
|
|
331
333
|
* @param {any} THREE
|
|
332
|
-
* @param {{ positionMil?: { x?: number, y?: number, z?: number }, board?: { widthMil?: number, heightMil?: number, thicknessMil?: number } }} placement
|
|
334
|
+
* @param {{ positionMil?: { x?: number, y?: number, z?: number }, board?: { widthMil?: number, heightMil?: number, thicknessMil?: number }, sourceFrameScale?: { y?: number } }} placement
|
|
333
335
|
* @param {any} modelGroup
|
|
334
336
|
* @returns {any}
|
|
335
337
|
*/
|
|
@@ -338,6 +340,7 @@ export class PcbScene3dExternalModels {
|
|
|
338
340
|
const positionMil = placement?.positionMil || {}
|
|
339
341
|
|
|
340
342
|
PcbScene3dBoardAssemblyPresentation.apply(modelGroup, placement?.board)
|
|
343
|
+
PcbScene3dBoardAssemblyTransform.apply(modelGroup, placement)
|
|
341
344
|
wrapperGroup.position.set(
|
|
342
345
|
Number(positionMil.x || 0),
|
|
343
346
|
Number(positionMil.y || 0),
|
|
@@ -720,14 +723,28 @@ export class PcbScene3dExternalModels {
|
|
|
720
723
|
const geometry = new THREE.BufferGeometry()
|
|
721
724
|
geometry.setAttribute(
|
|
722
725
|
'position',
|
|
723
|
-
|
|
726
|
+
PcbScene3dBufferAttributeFactory.createFloat32(
|
|
727
|
+
THREE,
|
|
728
|
+
meshPayload.positions,
|
|
729
|
+
3
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
geometry.setIndex(
|
|
733
|
+
PcbScene3dBufferAttributeFactory.createUint32(
|
|
734
|
+
THREE,
|
|
735
|
+
meshPayload.indices,
|
|
736
|
+
1
|
|
737
|
+
)
|
|
724
738
|
)
|
|
725
|
-
geometry.setIndex(meshPayload.indices)
|
|
726
739
|
|
|
727
740
|
if (meshPayload.normals.length) {
|
|
728
741
|
geometry.setAttribute(
|
|
729
742
|
'normal',
|
|
730
|
-
|
|
743
|
+
PcbScene3dBufferAttributeFactory.createFloat32(
|
|
744
|
+
THREE,
|
|
745
|
+
meshPayload.normals,
|
|
746
|
+
3
|
|
747
|
+
)
|
|
731
748
|
)
|
|
732
749
|
} else {
|
|
733
750
|
geometry.computeVertexNormals()
|
|
@@ -752,7 +769,7 @@ export class PcbScene3dExternalModels {
|
|
|
752
769
|
/**
|
|
753
770
|
* Measures raw STEP mesh bounds in mil before the group-level unit scale is
|
|
754
771
|
* applied.
|
|
755
|
-
* @param {{ positions?: number
|
|
772
|
+
* @param {{ positions?: ArrayLike<number> }[]} meshPayloads STEP mesh payloads.
|
|
756
773
|
* @returns {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, centerX: number, centerY: number, centerZ: number, sizeX: number, sizeY: number, sizeZ: number } | null}
|
|
757
774
|
*/
|
|
758
775
|
static #measureSourceBoundsMil(meshPayloads) {
|
|
@@ -765,7 +782,9 @@ export class PcbScene3dExternalModels {
|
|
|
765
782
|
|
|
766
783
|
;(Array.isArray(meshPayloads) ? meshPayloads : []).forEach(
|
|
767
784
|
(meshPayload) => {
|
|
768
|
-
;(
|
|
785
|
+
;(PcbScene3dBufferAttributeFactory.isNumberSequence(
|
|
786
|
+
meshPayload?.positions
|
|
787
|
+
)
|
|
769
788
|
? meshPayload.positions
|
|
770
789
|
: []
|
|
771
790
|
).forEach((value, index) => {
|
|
@@ -817,7 +836,7 @@ export class PcbScene3dExternalModels {
|
|
|
817
836
|
* when the importer exposes them.
|
|
818
837
|
* @param {any} THREE
|
|
819
838
|
* @param {any} geometry
|
|
820
|
-
* @param {{ color?: number[] | null, indices?: number
|
|
839
|
+
* @param {{ color?: number[] | null, indices?: ArrayLike<number>, faceColors?: { first: number, last: number, color: number[] | null }[] }} meshPayload
|
|
821
840
|
* @returns {any[]}
|
|
822
841
|
*/
|
|
823
842
|
static #buildStepMeshMaterials(THREE, geometry, meshPayload) {
|
|
@@ -886,14 +905,12 @@ export class PcbScene3dExternalModels {
|
|
|
886
905
|
/**
|
|
887
906
|
* Applies grouped material ranges for face-colored STEP triangles.
|
|
888
907
|
* @param {any} geometry
|
|
889
|
-
* @param {number
|
|
908
|
+
* @param {ArrayLike<number>} indices
|
|
890
909
|
* @param {{ first: number, last: number }[]} faceColors
|
|
891
910
|
* @returns {void}
|
|
892
911
|
*/
|
|
893
912
|
static #applyFaceColorGroups(geometry, indices, faceColors) {
|
|
894
|
-
const triangleCount = Math.floor(
|
|
895
|
-
(Array.isArray(indices) ? indices.length : 0) / 3
|
|
896
|
-
)
|
|
913
|
+
const triangleCount = Math.floor(Number(indices?.length || 0) / 3)
|
|
897
914
|
let triangleIndex = 0
|
|
898
915
|
let faceColorIndex = 0
|
|
899
916
|
|
|
@@ -929,15 +946,13 @@ export class PcbScene3dExternalModels {
|
|
|
929
946
|
/**
|
|
930
947
|
* Returns true when one face-color range overlaps valid triangle indices.
|
|
931
948
|
* @param {{ first?: number, last?: number }} faceColor
|
|
932
|
-
* @param {number
|
|
949
|
+
* @param {ArrayLike<number> | undefined} indices
|
|
933
950
|
* @returns {boolean}
|
|
934
951
|
*/
|
|
935
952
|
static #isValidFaceColorRange(faceColor, indices) {
|
|
936
953
|
const first = Number(faceColor?.first)
|
|
937
954
|
const last = Number(faceColor?.last)
|
|
938
|
-
const triangleCount = Math.floor(
|
|
939
|
-
(Array.isArray(indices) ? indices.length : 0) / 3
|
|
940
|
-
)
|
|
955
|
+
const triangleCount = Math.floor(Number(indices?.length || 0) / 3)
|
|
941
956
|
|
|
942
957
|
return (
|
|
943
958
|
Number.isInteger(first) &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFactory.mjs'
|
|
2
|
+
import { PcbScene3dBodyColor } from './PcbScene3dBodyColor.mjs'
|
|
2
3
|
import { PcbScene3dCameraRig } from './PcbScene3dCameraRig.mjs'
|
|
3
4
|
import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
|
|
4
5
|
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
@@ -12,7 +13,7 @@ import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
|
|
|
12
13
|
import { PcbScene3dPresetState } from './PcbScene3dPresetState.mjs'
|
|
13
14
|
import { PcbScene3dRenderGroupVisibility } from './PcbScene3dRenderGroupVisibility.mjs'
|
|
14
15
|
import { PcbScene3dRuntimeBoardMeshes } from './PcbScene3dRuntimeBoardMeshes.mjs'
|
|
15
|
-
import {
|
|
16
|
+
import { PcbScene3dSilkscreenChunkedFactory } from './PcbScene3dSilkscreenChunkedFactory.mjs'
|
|
16
17
|
import { PcbScene3dTrueTypeTextFactory } from './PcbScene3dTrueTypeTextFactory.mjs'
|
|
17
18
|
import { PcbScene3dSelectionStyler } from './PcbScene3dSelectionStyler.mjs'
|
|
18
19
|
import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
|
|
@@ -340,6 +341,12 @@ export class PcbScene3dRuntime {
|
|
|
340
341
|
)
|
|
341
342
|
)
|
|
342
343
|
this.#groups.set('board', boardGroup)
|
|
344
|
+
PcbScene3dRuntimeBoardMeshes.applyBoardFaceSide(
|
|
345
|
+
this.#three,
|
|
346
|
+
boardGroup,
|
|
347
|
+
this.#presetState.get(),
|
|
348
|
+
this.#sceneDescription
|
|
349
|
+
)
|
|
343
350
|
this.#rootGroup.add(boardGroup)
|
|
344
351
|
const silkscreenGroup = new THREE.Group()
|
|
345
352
|
this.#groups.set('silkscreen', silkscreenGroup)
|
|
@@ -376,14 +383,18 @@ export class PcbScene3dRuntime {
|
|
|
376
383
|
* @returns {void}
|
|
377
384
|
*/
|
|
378
385
|
#applyViewScale(preset) {
|
|
379
|
-
if (!this.#viewOrientationGroup)
|
|
380
|
-
return
|
|
381
|
-
}
|
|
386
|
+
if (!this.#viewOrientationGroup) return
|
|
382
387
|
const scale = PcbScene3dRuntime.resolveViewScale(
|
|
383
388
|
preset,
|
|
384
389
|
this.#sceneDescription
|
|
385
390
|
)
|
|
386
391
|
this.#viewOrientationGroup.scale.set(scale.x, scale.y, scale.z)
|
|
392
|
+
PcbScene3dRuntimeBoardMeshes.applyBoardFaceSide(
|
|
393
|
+
this.#three,
|
|
394
|
+
this.#groups.get('board'),
|
|
395
|
+
preset,
|
|
396
|
+
this.#sceneDescription
|
|
397
|
+
)
|
|
387
398
|
PcbScene3dExternalModels.applyViewCompensation(
|
|
388
399
|
this.#groups.get('silkscreen'),
|
|
389
400
|
scale
|
|
@@ -448,22 +459,24 @@ export class PcbScene3dRuntime {
|
|
|
448
459
|
*/
|
|
449
460
|
async #loadDeferredSilkscreen() {
|
|
450
461
|
const silkscreenGroup = this.#groups.get('silkscreen')
|
|
451
|
-
if (!silkscreenGroup || silkscreenGroup.children.length)
|
|
452
|
-
return
|
|
453
|
-
}
|
|
462
|
+
if (!silkscreenGroup || silkscreenGroup.children.length) return
|
|
454
463
|
|
|
455
464
|
await PcbScene3dTrueTypeTextFactory.prepareEmbeddedFonts(
|
|
456
465
|
this.#sceneDescription.detail.embeddedFonts || []
|
|
457
466
|
)
|
|
458
|
-
|
|
459
|
-
const detailGroup = PcbScene3dSilkscreenFactory.buildGroup(
|
|
467
|
+
const detailGroup = await PcbScene3dSilkscreenChunkedFactory.buildGroup(
|
|
460
468
|
this.#three,
|
|
461
469
|
this.#sceneDescription.detail.silkscreen || {},
|
|
462
470
|
this.#sceneDescription.board.thicknessMil / 2 + 1.18,
|
|
463
471
|
-(this.#sceneDescription.board.thicknessMil / 2 + 1.18),
|
|
464
|
-
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
472
|
+
(x, y) => this.#normalizeDetailPoint(x, y),
|
|
473
|
+
{
|
|
474
|
+
shouldContinue: () => !this.#isDisposed,
|
|
475
|
+
yieldToMain: () => PcbScene3dRuntime.#yieldToNextFrame()
|
|
476
|
+
}
|
|
465
477
|
)
|
|
466
478
|
|
|
479
|
+
if (this.#isDisposed) return
|
|
467
480
|
if (detailGroup.children.length) {
|
|
468
481
|
silkscreenGroup.add(detailGroup)
|
|
469
482
|
this.#applyViewScale(this.#presetState.get())
|
|
@@ -526,7 +539,7 @@ export class PcbScene3dRuntime {
|
|
|
526
539
|
const family = component.body.family
|
|
527
540
|
const size = component.body.sizeMil
|
|
528
541
|
const material = new THREE.MeshStandardMaterial({
|
|
529
|
-
color:
|
|
542
|
+
color: PcbScene3dBodyColor.resolve(family),
|
|
530
543
|
roughness: 0.72,
|
|
531
544
|
metalness: family === 'chip' ? 0.12 : 0.08
|
|
532
545
|
})
|
|
@@ -971,26 +984,4 @@ export class PcbScene3dRuntime {
|
|
|
971
984
|
const dy = Number(end?.y || 0) - Number(start?.y || 0)
|
|
972
985
|
return Math.hypot(dx, dy)
|
|
973
986
|
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Resolves a simple body color by package family.
|
|
977
|
-
* @param {string} family
|
|
978
|
-
* @returns {number}
|
|
979
|
-
*/
|
|
980
|
-
static #resolveBodyColor(family) {
|
|
981
|
-
if (family === 'radial-capacitor') {
|
|
982
|
-
return 0xa60f10
|
|
983
|
-
}
|
|
984
|
-
if (family === 'connector-block') {
|
|
985
|
-
return 0xd5d6da
|
|
986
|
-
}
|
|
987
|
-
if (family === 'test-point') {
|
|
988
|
-
return 0x0ea5a8
|
|
989
|
-
}
|
|
990
|
-
if (family === 'chip') {
|
|
991
|
-
return 0xf5f5ef
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
return 0x232428
|
|
995
|
-
}
|
|
996
987
|
}
|