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,255 @@
|
|
|
1
|
+
import { PcbScene3dBoardEdgeCutoutBuilder } from './PcbScene3dBoardEdgeCutoutBuilder.mjs'
|
|
2
|
+
import { PcbScene3dDrillCutoutFilter } from './PcbScene3dDrillCutoutFilter.mjs'
|
|
3
|
+
import { PcbScene3dPreparedPolygon } from './PcbScene3dPreparedPolygon.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map-compatible cache that prepares normalized cutouts on first consumption.
|
|
7
|
+
*/
|
|
8
|
+
class PcbScene3dLazyPreparedPolygonCache extends Map {
|
|
9
|
+
/** @type {(cutout: unknown) => void} */
|
|
10
|
+
#prepare
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a cache backed by one request-scoped preparation callback.
|
|
14
|
+
* @param {(cutout: unknown) => void} prepare Preparation callback.
|
|
15
|
+
*/
|
|
16
|
+
constructor(prepare) {
|
|
17
|
+
super()
|
|
18
|
+
this.#prepare = prepare
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns whether a source has a compatible preparation.
|
|
23
|
+
* @param {unknown} cutout Candidate source.
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
has(cutout) {
|
|
27
|
+
this.#prepareIfNeeded(cutout)
|
|
28
|
+
return super.has(cutout)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns one compatible preparation when available.
|
|
33
|
+
* @param {unknown} cutout Candidate source.
|
|
34
|
+
* @returns {PcbScene3dPreparedPolygon | undefined}
|
|
35
|
+
*/
|
|
36
|
+
get(cutout) {
|
|
37
|
+
this.#prepareIfNeeded(cutout)
|
|
38
|
+
return super.get(cutout)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prepares a source only when the cache lacks silkscreen capabilities.
|
|
43
|
+
* @param {unknown} cutout Candidate source.
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
#prepareIfNeeded(cutout) {
|
|
47
|
+
const prepared = super.get(cutout)
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
prepared?.circleDetectionEnabled !== true ||
|
|
51
|
+
prepared?.pointRepresentation !== 'raw-numeric'
|
|
52
|
+
) {
|
|
53
|
+
this.#prepare(cutout)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Owns exact prepared cutout metadata for one silkscreen side build.
|
|
60
|
+
*/
|
|
61
|
+
export class PcbScene3dSilkscreenCutoutContext {
|
|
62
|
+
static #GEOMETRY_EPSILON = 0.001
|
|
63
|
+
|
|
64
|
+
/** @type {Map<any, PcbScene3dPreparedPolygon>} */
|
|
65
|
+
#preparedPolygonCache = new PcbScene3dLazyPreparedPolygonCache((cutout) =>
|
|
66
|
+
this.#prepareNormalizedCutout(cutout)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the lazy cache shared by exact geometry consumers.
|
|
71
|
+
* @returns {Map<any, PcbScene3dPreparedPolygon>}
|
|
72
|
+
*/
|
|
73
|
+
get preparedPolygonCache() {
|
|
74
|
+
return this.#preparedPolygonCache
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolves one circular-enabled finite source preparation.
|
|
79
|
+
* @param {unknown} cutout Candidate normalized cutout points.
|
|
80
|
+
* @returns {PcbScene3dPreparedPolygon | null}
|
|
81
|
+
*/
|
|
82
|
+
resolve(cutout) {
|
|
83
|
+
if (!PcbScene3dSilkscreenCutoutContext.#isNormalizedCutout(cutout)) {
|
|
84
|
+
this.#preparedPolygonCache.delete(cutout)
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.#resolveNormalizedCutout(cutout)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolves one source already proven to be finite and normalized.
|
|
93
|
+
* @param {{ x: number, y: number }[]} cutout Normalized cutout points.
|
|
94
|
+
* @returns {PcbScene3dPreparedPolygon}
|
|
95
|
+
*/
|
|
96
|
+
#resolveNormalizedCutout(cutout) {
|
|
97
|
+
let prepared = Map.prototype.get.call(
|
|
98
|
+
this.#preparedPolygonCache,
|
|
99
|
+
cutout
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
prepared?.circleDetectionEnabled === true &&
|
|
104
|
+
prepared?.pointRepresentation === 'raw-numeric'
|
|
105
|
+
) {
|
|
106
|
+
return prepared
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
prepared = new PcbScene3dPreparedPolygon(cutout, {
|
|
110
|
+
source: cutout,
|
|
111
|
+
epsilon: PcbScene3dSilkscreenCutoutContext.#GEOMETRY_EPSILON,
|
|
112
|
+
detectCircle: true,
|
|
113
|
+
pointRepresentation: 'raw-numeric'
|
|
114
|
+
})
|
|
115
|
+
Map.prototype.set.call(this.#preparedPolygonCache, cutout, prepared)
|
|
116
|
+
return prepared
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prepares one finite normalized cutout requested by a real cache consumer.
|
|
121
|
+
* @param {unknown} cutout Candidate normalized cutout points.
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
124
|
+
#prepareNormalizedCutout(cutout) {
|
|
125
|
+
if (PcbScene3dSilkscreenCutoutContext.#isNormalizedCutout(cutout)) {
|
|
126
|
+
this.#resolveNormalizedCutout(cutout)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolves cached sampled-circle metadata for one normalized cutout.
|
|
132
|
+
* @param {unknown} cutout Candidate normalized cutout points.
|
|
133
|
+
* @returns {{ isCircular: true, centerX: number, centerY: number, radius: number } | null}
|
|
134
|
+
*/
|
|
135
|
+
resolveCircle(cutout) {
|
|
136
|
+
return this.resolve(cutout)?.circle ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns true when a cutout can safely become a shape hole.
|
|
141
|
+
* @param {{ x: number, y: number }[]} hole Cutout polygon.
|
|
142
|
+
* @param {{ x: number, y: number }[]} contour Fill contour.
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
isHoleInsideContour(hole, contour) {
|
|
146
|
+
const prepared = this.resolve(hole)
|
|
147
|
+
if (!prepared) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
|
|
152
|
+
hole,
|
|
153
|
+
contour,
|
|
154
|
+
prepared.circle
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Converts circular edge-crossing cutouts into one fill contour.
|
|
160
|
+
* @param {{ x: number, y: number }[]} contourPoints Fill contour.
|
|
161
|
+
* @param {{ x: number, y: number }[][]} cutouts Candidate cutouts.
|
|
162
|
+
* @returns {{ points: { x: number, y: number }[], appliedCutouts: { x: number, y: number }[][] }}
|
|
163
|
+
*/
|
|
164
|
+
applyCircularEdgeCutouts(contourPoints, cutouts) {
|
|
165
|
+
let points = contourPoints
|
|
166
|
+
const appliedCutouts = []
|
|
167
|
+
const candidates = PcbScene3dDrillCutoutFilter.removeNestedCutouts(
|
|
168
|
+
(Array.isArray(cutouts) ? cutouts : []).filter(
|
|
169
|
+
(cutout) =>
|
|
170
|
+
this.resolveCircle(cutout) &&
|
|
171
|
+
!this.isHoleInsideContour(cutout, contourPoints)
|
|
172
|
+
),
|
|
173
|
+
{ preparedPolygonCache: this.#preparedPolygonCache }
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for (const cutout of candidates) {
|
|
177
|
+
const circularCutout = this.resolveCircle(cutout)
|
|
178
|
+
if (!circularCutout || this.isHoleInsideContour(cutout, points)) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const nextPoints =
|
|
183
|
+
PcbScene3dBoardEdgeCutoutBuilder.applyCircularEdgeCutouts(
|
|
184
|
+
points,
|
|
185
|
+
[circularCutout]
|
|
186
|
+
)
|
|
187
|
+
if (
|
|
188
|
+
PcbScene3dSilkscreenCutoutContext.#samePointList(
|
|
189
|
+
points,
|
|
190
|
+
nextPoints
|
|
191
|
+
)
|
|
192
|
+
) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
points = nextPoints
|
|
197
|
+
appliedCutouts.push(cutout)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { points, appliedCutouts }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns true when a source is already a finite normalized polygon.
|
|
205
|
+
* @param {unknown} cutout Candidate point collection.
|
|
206
|
+
* @returns {cutout is { x: number, y: number }[]}
|
|
207
|
+
*/
|
|
208
|
+
static #isNormalizedCutout(cutout) {
|
|
209
|
+
if (!Array.isArray(cutout) || cutout.length < 3) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (let index = 0; index < cutout.length; index += 1) {
|
|
214
|
+
if (!Object.hasOwn(cutout, index)) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const point = cutout[index]
|
|
219
|
+
if (
|
|
220
|
+
typeof point?.x !== 'number' ||
|
|
221
|
+
!Number.isFinite(point.x) ||
|
|
222
|
+
typeof point?.y !== 'number' ||
|
|
223
|
+
!Number.isFinite(point.y)
|
|
224
|
+
) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns true when two point lists share identical coordinates.
|
|
234
|
+
* @param {{ x: number, y: number }[]} first First point list.
|
|
235
|
+
* @param {{ x: number, y: number }[]} second Second point list.
|
|
236
|
+
* @returns {boolean}
|
|
237
|
+
*/
|
|
238
|
+
static #samePointList(first, second) {
|
|
239
|
+
return (
|
|
240
|
+
Array.isArray(first) &&
|
|
241
|
+
Array.isArray(second) &&
|
|
242
|
+
first.length === second.length &&
|
|
243
|
+
first.every((point, index) => {
|
|
244
|
+
const otherPoint = second[index]
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
Math.abs(point.x - otherPoint.x) <=
|
|
248
|
+
PcbScene3dSilkscreenCutoutContext.#GEOMETRY_EPSILON &&
|
|
249
|
+
Math.abs(point.y - otherPoint.y) <=
|
|
250
|
+
PcbScene3dSilkscreenCutoutContext.#GEOMETRY_EPSILON
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|