pcb-scene3d-viewer 1.0.1 → 1.1.1
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/PcbScene3dBoardSolderMaskFactory.mjs +1 -1
- package/src/PcbScene3dBodyColor.mjs +26 -0
- package/src/PcbScene3dBufferAttributeFactory.mjs +128 -0
- package/src/PcbScene3dCircularCutoutOverlap.mjs +167 -0
- package/src/PcbScene3dCutoutCircleDetector.mjs +84 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +151 -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
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export class PcbScene3dBoardAssemblyPlacement {
|
|
5
5
|
/**
|
|
6
6
|
* Builds the synthetic placement for a full board assembly model.
|
|
7
|
-
* @param {{ board?: { widthMil?: number, heightMil?: number, thicknessMil?: number, surfaceColor?: number }, boardAssemblyModel?: any }} sceneDescription
|
|
7
|
+
* @param {{ board?: { widthMil?: number, heightMil?: number, thicknessMil?: number, surfaceColor?: number }, boardAssemblyModel?: any, sourceFormat?: string }} sceneDescription
|
|
8
8
|
* @returns {any | null}
|
|
9
9
|
*/
|
|
10
10
|
static build(sceneDescription) {
|
|
@@ -14,6 +14,15 @@ export class PcbScene3dBoardAssemblyPlacement {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const board = sceneDescription?.board || {}
|
|
17
|
+
const mirrorsSourceY =
|
|
18
|
+
String(sceneDescription?.sourceFormat || '').toLowerCase() ===
|
|
19
|
+
'altium'
|
|
20
|
+
const sourceFrameScale = {
|
|
21
|
+
x: 1,
|
|
22
|
+
y: mirrorsSourceY ? -1 : 1,
|
|
23
|
+
z: 1
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
return {
|
|
18
27
|
designator: 'Board assembly',
|
|
19
28
|
sourceType: 'board-assembly',
|
|
@@ -21,9 +30,12 @@ export class PcbScene3dBoardAssemblyPlacement {
|
|
|
21
30
|
rotationDeg: 0,
|
|
22
31
|
positionMil: {
|
|
23
32
|
x: -Number(board.widthMil || 0) / 2,
|
|
24
|
-
y:
|
|
33
|
+
y:
|
|
34
|
+
(mirrorsSourceY ? 1 : -1) *
|
|
35
|
+
(Number(board.heightMil || 0) / 2),
|
|
25
36
|
z: 0
|
|
26
37
|
},
|
|
38
|
+
sourceFrameScale,
|
|
27
39
|
board: {
|
|
28
40
|
widthMil: Number(board.widthMil || 0),
|
|
29
41
|
heightMil: Number(board.heightMil || 0),
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies source-frame transforms to full-board assembly STEP geometry.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dBoardAssemblyTransform {
|
|
5
|
+
/**
|
|
6
|
+
* Applies the source-frame transform encoded by a board assembly placement.
|
|
7
|
+
* @param {any} modelGroup Loaded board assembly group.
|
|
8
|
+
* @param {{ sourceFrameScale?: { y?: number } }} placement Placement metadata.
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
static apply(modelGroup, placement) {
|
|
12
|
+
if (!PcbScene3dBoardAssemblyTransform.#shouldMirrorY(placement)) {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
PcbScene3dBoardAssemblyTransform.#traverseModelObjects(
|
|
17
|
+
modelGroup,
|
|
18
|
+
(object) =>
|
|
19
|
+
PcbScene3dBoardAssemblyTransform.#mirrorGeometryY(
|
|
20
|
+
object?.geometry
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
modelGroup.userData = modelGroup.userData || {}
|
|
24
|
+
modelGroup.userData.scene3dBoardAssemblyMirroredY = true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks whether the assembly source frame needs a Y mirror.
|
|
29
|
+
* @param {{ sourceFrameScale?: { y?: number } }} placement Placement metadata.
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
static #shouldMirrorY(placement) {
|
|
33
|
+
return Number(placement?.sourceFrameScale?.y ?? 1) < 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mirrors one geometry around the source Y origin.
|
|
38
|
+
* @param {any} geometry Buffer geometry.
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
static #mirrorGeometryY(geometry) {
|
|
42
|
+
const position = PcbScene3dBoardAssemblyTransform.#getAttribute(
|
|
43
|
+
geometry,
|
|
44
|
+
'position'
|
|
45
|
+
)
|
|
46
|
+
if (!position?.array?.length) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
PcbScene3dBoardAssemblyTransform.#mirrorAttributeY(position)
|
|
51
|
+
const normal = PcbScene3dBoardAssemblyTransform.#getAttribute(
|
|
52
|
+
geometry,
|
|
53
|
+
'normal'
|
|
54
|
+
)
|
|
55
|
+
PcbScene3dBoardAssemblyTransform.#mirrorAttributeY(normal)
|
|
56
|
+
PcbScene3dBoardAssemblyTransform.#reverseTriangleWinding(geometry)
|
|
57
|
+
geometry.computeBoundingBox?.()
|
|
58
|
+
geometry.computeBoundingSphere?.()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mirrors the Y component of a vertex-like attribute.
|
|
63
|
+
* @param {{ array?: ArrayLike<number>, itemSize?: number, needsUpdate?: boolean } | null} attribute Geometry attribute.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
static #mirrorAttributeY(attribute) {
|
|
67
|
+
const itemSize = Number(attribute?.itemSize || 0)
|
|
68
|
+
if (!attribute?.array?.length || itemSize < 2) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (let index = 1; index < attribute.array.length; index += itemSize) {
|
|
73
|
+
attribute.array[index] = -Number(attribute.array[index] || 0)
|
|
74
|
+
}
|
|
75
|
+
attribute.needsUpdate = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reverses triangle winding after a one-axis mirror.
|
|
80
|
+
* @param {any} geometry Buffer geometry.
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
static #reverseTriangleWinding(geometry) {
|
|
84
|
+
const index = geometry?.index
|
|
85
|
+
const indexArray = index?.array
|
|
86
|
+
if (!indexArray?.length) {
|
|
87
|
+
PcbScene3dBoardAssemblyTransform.#reverseUnindexedWinding(geometry)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let offset = 0; offset + 2 < indexArray.length; offset += 3) {
|
|
92
|
+
const next = indexArray[offset + 1]
|
|
93
|
+
indexArray[offset + 1] = indexArray[offset + 2]
|
|
94
|
+
indexArray[offset + 2] = next
|
|
95
|
+
}
|
|
96
|
+
index.needsUpdate = true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reverses unindexed triangle winding across all geometry attributes.
|
|
101
|
+
* @param {any} geometry Buffer geometry.
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
static #reverseUnindexedWinding(geometry) {
|
|
105
|
+
Object.values(geometry?.attributes || {}).forEach((attribute) =>
|
|
106
|
+
PcbScene3dBoardAssemblyTransform.#swapTriangleVertices(
|
|
107
|
+
attribute,
|
|
108
|
+
1,
|
|
109
|
+
2
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Swaps two vertices inside each unindexed triangle for one attribute.
|
|
116
|
+
* @param {{ array?: ArrayLike<number>, itemSize?: number, needsUpdate?: boolean }} attribute Geometry attribute.
|
|
117
|
+
* @param {number} leftVertex First vertex offset inside the triangle.
|
|
118
|
+
* @param {number} rightVertex Second vertex offset inside the triangle.
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
static #swapTriangleVertices(attribute, leftVertex, rightVertex) {
|
|
122
|
+
const itemSize = Number(attribute?.itemSize || 0)
|
|
123
|
+
const array = attribute?.array
|
|
124
|
+
if (!array?.length || itemSize <= 0) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (
|
|
129
|
+
let offset = 0;
|
|
130
|
+
offset + itemSize * 3 <= array.length;
|
|
131
|
+
offset += itemSize * 3
|
|
132
|
+
) {
|
|
133
|
+
for (let item = 0; item < itemSize; item += 1) {
|
|
134
|
+
const left = offset + leftVertex * itemSize + item
|
|
135
|
+
const right = offset + rightVertex * itemSize + item
|
|
136
|
+
const value = array[left]
|
|
137
|
+
array[left] = array[right]
|
|
138
|
+
array[right] = value
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
attribute.needsUpdate = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns a geometry attribute across Three.js-compatible shapes.
|
|
146
|
+
* @param {any} geometry Buffer geometry.
|
|
147
|
+
* @param {string} name Attribute name.
|
|
148
|
+
* @returns {any}
|
|
149
|
+
*/
|
|
150
|
+
static #getAttribute(geometry, name) {
|
|
151
|
+
return typeof geometry?.getAttribute === 'function'
|
|
152
|
+
? geometry.getAttribute(name)
|
|
153
|
+
: geometry?.attributes?.[name]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Traverses model objects.
|
|
158
|
+
* @param {any} root Root object.
|
|
159
|
+
* @param {(object: any) => void} visitor Visitor callback.
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
static #traverseModelObjects(root, visitor) {
|
|
163
|
+
if (!root) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof root.traverse === 'function') {
|
|
168
|
+
root.traverse(visitor)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
visitor(root)
|
|
173
|
+
;(Array.isArray(root.children) ? root.children : []).forEach((child) =>
|
|
174
|
+
PcbScene3dBoardAssemblyTransform.#traverseModelObjects(
|
|
175
|
+
child,
|
|
176
|
+
visitor
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Builds rounded outer-contour notches for drills that intersect board edges.
|
|
3
5
|
*/
|
|
@@ -5,6 +7,7 @@ export class PcbScene3dBoardEdgeCutoutBuilder {
|
|
|
5
7
|
static #OUTER_SAMPLE_POINTS = 160
|
|
6
8
|
static #EDGE_CUTOUT_SAMPLE_POINTS = 72
|
|
7
9
|
static #GEOMETRY_EPSILON = 0.001
|
|
10
|
+
static #CONTOUR_CACHE = new WeakMap()
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Resolves sampled points from one shape outline.
|
|
@@ -98,18 +101,128 @@ export class PcbScene3dBoardEdgeCutoutBuilder {
|
|
|
98
101
|
* @returns {boolean}
|
|
99
102
|
*/
|
|
100
103
|
static isHoleInsideContour(hole, contour) {
|
|
101
|
-
|
|
102
|
-
Array.isArray(hole)
|
|
103
|
-
Array.isArray(contour)
|
|
104
|
-
hole.length
|
|
105
|
-
contour.length
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
if (
|
|
105
|
+
!Array.isArray(hole) ||
|
|
106
|
+
!Array.isArray(contour) ||
|
|
107
|
+
hole.length < 3 ||
|
|
108
|
+
contour.length < 3
|
|
109
|
+
) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const circularHole = PcbScene3dCutoutCircleDetector.resolve(
|
|
114
|
+
hole,
|
|
115
|
+
PcbScene3dBoardEdgeCutoutBuilder.#GEOMETRY_EPSILON
|
|
116
|
+
)
|
|
117
|
+
if (circularHole) {
|
|
118
|
+
return PcbScene3dBoardEdgeCutoutBuilder.#isCircularHoleInsideContour(
|
|
119
|
+
circularHole,
|
|
120
|
+
contour
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return hole.every((point) =>
|
|
125
|
+
PcbScene3dBoardEdgeCutoutBuilder.#isPointStrictlyInsidePolygon(
|
|
126
|
+
point,
|
|
127
|
+
contour
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns true when a circular hole lies fully inside a contour.
|
|
134
|
+
* @param {{ centerX: number, centerY: number, radius: number }} hole
|
|
135
|
+
* @param {{ x: number, y: number }[]} contour
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
static #isCircularHoleInsideContour(hole, contour) {
|
|
139
|
+
const center = { x: hole.centerX, y: hole.centerY }
|
|
140
|
+
if (
|
|
141
|
+
!PcbScene3dBoardEdgeCutoutBuilder.#isPointStrictlyInsidePolygon(
|
|
142
|
+
center,
|
|
143
|
+
contour
|
|
144
|
+
)
|
|
145
|
+
) {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const preparedContour =
|
|
150
|
+
PcbScene3dBoardEdgeCutoutBuilder.#resolvePreparedContour(contour)
|
|
151
|
+
const radius =
|
|
152
|
+
Number(hole.radius || 0) +
|
|
153
|
+
PcbScene3dBoardEdgeCutoutBuilder.#GEOMETRY_EPSILON
|
|
154
|
+
const radiusSquared = radius * radius
|
|
155
|
+
|
|
156
|
+
return preparedContour.segments.every(
|
|
157
|
+
(segment) =>
|
|
158
|
+
PcbScene3dBoardEdgeCutoutBuilder.#distanceSquaredToSegment(
|
|
159
|
+
center,
|
|
160
|
+
segment
|
|
161
|
+
) > radiusSquared
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolves cached contour segment metadata.
|
|
167
|
+
* @param {{ x: number, y: number }[]} contour
|
|
168
|
+
* @returns {{ segments: { start: { x: number, y: number }, dx: number, dy: number, lengthSquared: number }[] }}
|
|
169
|
+
*/
|
|
170
|
+
static #resolvePreparedContour(contour) {
|
|
171
|
+
const cached =
|
|
172
|
+
PcbScene3dBoardEdgeCutoutBuilder.#CONTOUR_CACHE.get(contour)
|
|
173
|
+
if (cached) {
|
|
174
|
+
return cached
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const prepared = {
|
|
178
|
+
segments: contour.map((start, index) => {
|
|
179
|
+
const end = contour[(index + 1) % contour.length]
|
|
180
|
+
const dx = end.x - start.x
|
|
181
|
+
const dy = end.y - start.y
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
start,
|
|
185
|
+
dx,
|
|
186
|
+
dy,
|
|
187
|
+
lengthSquared: dx * dx + dy * dy
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
PcbScene3dBoardEdgeCutoutBuilder.#CONTOUR_CACHE.set(contour, prepared)
|
|
192
|
+
return prepared
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolves squared distance from a point to a segment.
|
|
197
|
+
* @param {{ x: number, y: number }} point
|
|
198
|
+
* @param {{ start: { x: number, y: number }, dx: number, dy: number, lengthSquared: number }} segment
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
static #distanceSquaredToSegment(point, segment) {
|
|
202
|
+
if (
|
|
203
|
+
segment.lengthSquared <=
|
|
204
|
+
PcbScene3dBoardEdgeCutoutBuilder.#GEOMETRY_EPSILON
|
|
205
|
+
) {
|
|
206
|
+
const dx = point.x - segment.start.x
|
|
207
|
+
const dy = point.y - segment.start.y
|
|
208
|
+
return dx * dx + dy * dy
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const t = Math.max(
|
|
212
|
+
0,
|
|
213
|
+
Math.min(
|
|
214
|
+
1,
|
|
215
|
+
((point.x - segment.start.x) * segment.dx +
|
|
216
|
+
(point.y - segment.start.y) * segment.dy) /
|
|
217
|
+
segment.lengthSquared
|
|
111
218
|
)
|
|
112
219
|
)
|
|
220
|
+
const closestX = segment.start.x + segment.dx * t
|
|
221
|
+
const closestY = segment.start.y + segment.dy * t
|
|
222
|
+
const dx = point.x - closestX
|
|
223
|
+
const dy = point.y - closestY
|
|
224
|
+
|
|
225
|
+
return dx * dx + dy * dy
|
|
113
226
|
}
|
|
114
227
|
|
|
115
228
|
/**
|
|
@@ -6,7 +6,7 @@ import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
|
6
6
|
* Builds the board solid profile, including drilled holes.
|
|
7
7
|
*/
|
|
8
8
|
export class PcbScene3dBoardShapeFactory {
|
|
9
|
-
static #CURVE_SEGMENTS =
|
|
9
|
+
static #CURVE_SEGMENTS = 8
|
|
10
10
|
static #PAD_HOLE_SHAPE_SLOT = 2
|
|
11
11
|
static #PLATED_WALL_MATERIAL_INDEX = 2
|
|
12
12
|
static #EDGE_WALL_MATERIAL_INDEX = 1
|
|
@@ -322,16 +322,32 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
322
322
|
* @param {any} THREE
|
|
323
323
|
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
324
324
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
325
|
-
* @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 } }[]}
|
|
325
|
+
* @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 }[]}
|
|
326
326
|
*/
|
|
327
327
|
static #resolvePlatedContours(THREE, detail, normalizeBoardPoint) {
|
|
328
328
|
return PcbScene3dBoardShapeFactory.#resolvePlatedDrillSpecs(detail)
|
|
329
329
|
.map((drillSpec) => {
|
|
330
330
|
const point = normalizeBoardPoint(drillSpec.x, drillSpec.y)
|
|
331
|
-
const
|
|
331
|
+
const normalizedSpec = {
|
|
332
332
|
...drillSpec,
|
|
333
333
|
x: point.x,
|
|
334
334
|
y: point.y
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
PcbScene3dBoardShapeFactory.#isCircularDrillSpec(
|
|
339
|
+
normalizedSpec
|
|
340
|
+
)
|
|
341
|
+
) {
|
|
342
|
+
return PcbScene3dBoardShapeFactory.#buildCircularContour(
|
|
343
|
+
Number(normalizedSpec.x || 0),
|
|
344
|
+
Number(normalizedSpec.y || 0),
|
|
345
|
+
Number(normalizedSpec.diameter || 0) / 2
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const path = PcbScene3dDrillPathFactory.buildDrillPath(THREE, {
|
|
350
|
+
...normalizedSpec
|
|
335
351
|
})
|
|
336
352
|
const points =
|
|
337
353
|
path?.getPoints?.(
|
|
@@ -342,6 +358,51 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
342
358
|
.filter(Boolean)
|
|
343
359
|
}
|
|
344
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Returns true when a drill aperture can be matched as a circle.
|
|
363
|
+
* @param {{ diameter?: number, slotLength?: number | null }} drillSpec
|
|
364
|
+
* @returns {boolean}
|
|
365
|
+
*/
|
|
366
|
+
static #isCircularDrillSpec(drillSpec) {
|
|
367
|
+
const diameter = Number(drillSpec?.diameter || 0)
|
|
368
|
+
const slotLength = Number(drillSpec?.slotLength || 0)
|
|
369
|
+
|
|
370
|
+
return diameter > 0 && slotLength <= diameter + 0.001
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Builds a contour descriptor for circular plated holes.
|
|
375
|
+
* @param {number} centerX
|
|
376
|
+
* @param {number} centerY
|
|
377
|
+
* @param {number} radius
|
|
378
|
+
* @returns {{ points: { x: number, y: number }[], segments: any[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular: boolean, centerX: number, centerY: number, radius: number } | null}
|
|
379
|
+
*/
|
|
380
|
+
static #buildCircularContour(centerX, centerY, radius) {
|
|
381
|
+
if (
|
|
382
|
+
!Number.isFinite(centerX) ||
|
|
383
|
+
!Number.isFinite(centerY) ||
|
|
384
|
+
!Number.isFinite(radius) ||
|
|
385
|
+
radius <= 0
|
|
386
|
+
) {
|
|
387
|
+
return null
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
points: [],
|
|
392
|
+
segments: [],
|
|
393
|
+
bounds: {
|
|
394
|
+
minX: centerX - radius,
|
|
395
|
+
maxX: centerX + radius,
|
|
396
|
+
minY: centerY - radius,
|
|
397
|
+
maxY: centerY + radius
|
|
398
|
+
},
|
|
399
|
+
isCircular: true,
|
|
400
|
+
centerX,
|
|
401
|
+
centerY,
|
|
402
|
+
radius
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
345
406
|
/**
|
|
346
407
|
* Resolves deduped drill specs that represent plated holes.
|
|
347
408
|
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
@@ -707,7 +768,7 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
707
768
|
/**
|
|
708
769
|
* Returns true when all triangle vertices lie on one drill contour.
|
|
709
770
|
* @param {{ x: number, y: number }[]} points
|
|
710
|
-
* @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 } }} contour
|
|
771
|
+
* @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 }} contour
|
|
711
772
|
* @returns {boolean}
|
|
712
773
|
*/
|
|
713
774
|
static #matchesContourPoints(points, contour) {
|
|
@@ -774,7 +835,7 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
774
835
|
/**
|
|
775
836
|
* Returns true when a point is close to one contour boundary.
|
|
776
837
|
* @param {{ x: number, y: number }} point
|
|
777
|
-
* @param {{ 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 } }} contour
|
|
838
|
+
* @param {{ 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 }} contour
|
|
778
839
|
* @returns {boolean}
|
|
779
840
|
*/
|
|
780
841
|
static #isPointNearContour(point, contour) {
|
|
@@ -790,6 +851,14 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
790
851
|
return false
|
|
791
852
|
}
|
|
792
853
|
|
|
854
|
+
if (contour.isCircular) {
|
|
855
|
+
return PcbScene3dBoardShapeFactory.#isPointNearCircularContour(
|
|
856
|
+
point,
|
|
857
|
+
contour,
|
|
858
|
+
tolerance
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
793
862
|
for (const segment of contour.segments) {
|
|
794
863
|
if (
|
|
795
864
|
point.x < segment.bounds.minX - tolerance ||
|
|
@@ -813,6 +882,27 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
813
882
|
return false
|
|
814
883
|
}
|
|
815
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Returns true when a point lies within the tolerance ring of a circle.
|
|
887
|
+
* @param {{ x: number, y: number }} point
|
|
888
|
+
* @param {{ centerX?: number, centerY?: number, radius?: number }} contour
|
|
889
|
+
* @param {number} tolerance
|
|
890
|
+
* @returns {boolean}
|
|
891
|
+
*/
|
|
892
|
+
static #isPointNearCircularContour(point, contour, tolerance) {
|
|
893
|
+
const radius = Number(contour.radius || 0)
|
|
894
|
+
const minRadius = Math.max(0, radius - tolerance)
|
|
895
|
+
const maxRadius = radius + tolerance
|
|
896
|
+
const dx = point.x - Number(contour.centerX || 0)
|
|
897
|
+
const dy = point.y - Number(contour.centerY || 0)
|
|
898
|
+
const distanceSquared = dx * dx + dy * dy
|
|
899
|
+
|
|
900
|
+
return (
|
|
901
|
+
distanceSquared >= minRadius * minRadius &&
|
|
902
|
+
distanceSquared <= maxRadius * maxRadius
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
|
|
816
906
|
/**
|
|
817
907
|
* Appends or extends a contiguous material group.
|
|
818
908
|
* @param {{ start: number, count: number, materialIndex: number }[]} groups
|
|
@@ -7,7 +7,7 @@ import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
|
7
7
|
* Builds separate solder-mask face sheets for board-assembly rendering.
|
|
8
8
|
*/
|
|
9
9
|
export class PcbScene3dBoardSolderMaskFactory {
|
|
10
|
-
static #CURVE_SEGMENTS =
|
|
10
|
+
static #CURVE_SEGMENTS = 8
|
|
11
11
|
static #OUTER_SAMPLE_POINTS = 160
|
|
12
12
|
static #DRILL_SAMPLE_POINTS = 72
|
|
13
13
|
static #EDGE_CLEARANCE_MIL = 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves package body colors for generated fallback meshes.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dBodyColor {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a simple body color by package family.
|
|
7
|
+
* @param {string} family Package family.
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
static resolve(family) {
|
|
11
|
+
if (family === 'radial-capacitor') {
|
|
12
|
+
return 0xa60f10
|
|
13
|
+
}
|
|
14
|
+
if (family === 'connector-block') {
|
|
15
|
+
return 0xd5d6da
|
|
16
|
+
}
|
|
17
|
+
if (family === 'test-point') {
|
|
18
|
+
return 0x0ea5a8
|
|
19
|
+
}
|
|
20
|
+
if (family === 'chip') {
|
|
21
|
+
return 0xf5f5ef
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 0x232428
|
|
25
|
+
}
|
|
26
|
+
}
|