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.
Files changed (63) hide show
  1. package/README.md +46 -21
  2. package/docs/api.md +109 -8
  3. package/docs/circuitjson.md +143 -29
  4. package/docs/model-format.md +50 -7
  5. package/docs/release-notes-v1.2.0.md +122 -0
  6. package/docs/testing.md +30 -1
  7. package/package.json +6 -3
  8. package/spec/library-scope.md +10 -2
  9. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  10. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  12. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  13. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  14. package/src/PcbModelArchiveExporter.mjs +28 -112
  15. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  16. package/src/PcbScene3dAabbIndex.mjs +464 -0
  17. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
  18. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  19. package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
  23. package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
  24. package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
  25. package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
  26. package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
  27. package/src/PcbScene3dController.mjs +40 -34
  28. package/src/PcbScene3dCopperFactory.mjs +36 -22
  29. package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
  30. package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
  31. package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
  32. package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
  33. package/src/PcbScene3dCopperTextFactory.mjs +12 -4
  34. package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
  35. package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
  36. package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
  37. package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
  38. package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
  39. package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
  40. package/src/PcbScene3dDrillPathFactory.mjs +86 -16
  41. package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
  46. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
  47. package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
  48. package/src/PcbScene3dModelContent.mjs +236 -0
  49. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  50. package/src/PcbScene3dModelIdentity.mjs +106 -0
  51. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  52. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  53. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  54. package/src/PcbScene3dRuntime.mjs +45 -45
  55. package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
  56. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  57. package/src/PcbScene3dShellRenderer.mjs +75 -6
  58. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  59. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  60. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  61. package/src/PcbScene3dStepLoader.mjs +11 -10
  62. package/src/PcbScene3dText.mjs +1 -1
  63. 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
+ }