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,204 @@
1
+ import { PcbScene3dAabbIndex } from './PcbScene3dAabbIndex.mjs'
2
+ import { PcbScene3dPreparedPolygon } from './PcbScene3dPreparedPolygon.mjs'
3
+
4
+ /**
5
+ * Immutable request-scoped acceleration for normalized copper fill loop sets.
6
+ */
7
+ export class PcbScene3dCopperFillCoverageContext {
8
+ static #GEOMETRY_EPSILON = 0.001
9
+
10
+ /** @type {{ outer: PcbScene3dPreparedPolygon, holes: PcbScene3dPreparedPolygon[], bounds: object, sourceIndex: number }[]} */
11
+ #areas
12
+
13
+ /** @type {PcbScene3dAabbIndex} */
14
+ #index
15
+
16
+ /** @type {object[]} */
17
+ #loopSets
18
+
19
+ /** @type {object[] | null} */
20
+ #sourceLoopSets
21
+
22
+ /**
23
+ * Creates one context from already-normalized ordered loop sets.
24
+ * @param {{ outer?: number[][], holes?: number[][][] }[]} loopSets Ordered fill islands.
25
+ * @returns {PcbScene3dCopperFillCoverageContext}
26
+ */
27
+ static fromLoopSets(loopSets) {
28
+ return new PcbScene3dCopperFillCoverageContext(loopSets)
29
+ }
30
+
31
+ /**
32
+ * Prepares every loop coordinate once and builds one top-level area index.
33
+ * @param {{ outer?: number[][], holes?: number[][][] }[]} loopSets Ordered fill islands.
34
+ */
35
+ constructor(loopSets) {
36
+ this.#sourceLoopSets = Array.isArray(loopSets) ? loopSets : null
37
+ this.#loopSets = Object.freeze(Array.from(loopSets || []))
38
+ this.#areas = Object.freeze(
39
+ Array.from(this.#loopSets, (loopSet, sourceIndex) =>
40
+ PcbScene3dCopperFillCoverageContext.#prepareArea(
41
+ loopSet,
42
+ sourceIndex
43
+ )
44
+ )
45
+ )
46
+ this.#index = new PcbScene3dAabbIndex(this.#areas, {
47
+ resolveBounds: PcbScene3dCopperFillCoverageContext.#resolveBounds,
48
+ resolveSourceIndex:
49
+ PcbScene3dCopperFillCoverageContext.#resolveSourceIndex
50
+ })
51
+ }
52
+
53
+ /**
54
+ * Returns the immutable number of prepared fill areas.
55
+ * @returns {number}
56
+ */
57
+ get areaCount() {
58
+ return this.#areas.length
59
+ }
60
+
61
+ /**
62
+ * Returns whether the request array and loop-set order match preparation.
63
+ * @param {object[]} loopSets Active normalized fill islands.
64
+ * @returns {boolean}
65
+ */
66
+ matchesLoopSets(loopSets) {
67
+ if (
68
+ !Array.isArray(loopSets) ||
69
+ loopSets !== this.#sourceLoopSets ||
70
+ loopSets.length !== this.#loopSets.length
71
+ ) {
72
+ return false
73
+ }
74
+
75
+ for (let index = 0; index < loopSets.length; index += 1) {
76
+ if (loopSets[index] !== this.#loopSets[index]) {
77
+ return false
78
+ }
79
+ }
80
+
81
+ return true
82
+ }
83
+
84
+ /**
85
+ * Appends stable broad-phase area candidates into a caller-owned target.
86
+ * @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds Triangle bounds.
87
+ * @param {object[]} [target] Candidate accumulator.
88
+ * @param {{ beforeSourceIndex?: number, allowedSourceIndexes?: Set<number> | number[] | null }} [options] Source-order filters.
89
+ * @returns {object[]}
90
+ */
91
+ queryAreas(triangleBounds, target = [], options = {}) {
92
+ const candidates = this.#index.query(triangleBounds, {
93
+ epsilon: PcbScene3dCopperFillCoverageContext.#GEOMETRY_EPSILON,
94
+ stable: true
95
+ })
96
+ const beforeSourceIndex = options.beforeSourceIndex ?? Infinity
97
+ const allowedSourceIndexes = options.allowedSourceIndexes ?? null
98
+
99
+ for (const area of candidates) {
100
+ if (
101
+ area.sourceIndex < beforeSourceIndex &&
102
+ PcbScene3dCopperFillCoverageContext.#isAllowedSourceIndex(
103
+ area.sourceIndex,
104
+ allowedSourceIndexes
105
+ )
106
+ ) {
107
+ target.push(area)
108
+ }
109
+ }
110
+
111
+ return target
112
+ }
113
+
114
+ /**
115
+ * Prepares one outer loop and its authored holes.
116
+ * @param {{ outer?: number[][], holes?: number[][][] }} loopSet Source loop set.
117
+ * @param {number} sourceIndex Flattened source position.
118
+ * @returns {{ outer: PcbScene3dPreparedPolygon, holes: PcbScene3dPreparedPolygon[], bounds: object, sourceIndex: number }}
119
+ */
120
+ static #prepareArea(loopSet, sourceIndex) {
121
+ const outer = PcbScene3dCopperFillCoverageContext.#prepareLoop(
122
+ loopSet?.outer,
123
+ sourceIndex
124
+ )
125
+ const holes = Object.freeze(
126
+ Array.from(loopSet?.holes || [], (hole) =>
127
+ PcbScene3dCopperFillCoverageContext.#prepareLoop(
128
+ hole,
129
+ sourceIndex
130
+ )
131
+ )
132
+ )
133
+
134
+ return Object.freeze({
135
+ outer,
136
+ holes,
137
+ bounds: outer.bounds,
138
+ sourceIndex
139
+ })
140
+ }
141
+
142
+ /**
143
+ * Converts one normalized pair loop to numeric point objects once.
144
+ * @param {number[][] | undefined} loop Source loop.
145
+ * @param {number} sourceIndex Flattened source position.
146
+ * @returns {PcbScene3dPreparedPolygon}
147
+ */
148
+ static #prepareLoop(loop, sourceIndex) {
149
+ const source = Array.isArray(loop) ? loop : []
150
+ const points = Object.freeze(
151
+ source.map((point) => {
152
+ const x = Number(point?.[0])
153
+ const y = Number(point?.[1])
154
+ return Object.freeze({ x, y })
155
+ })
156
+ )
157
+
158
+ return new PcbScene3dPreparedPolygon(points, {
159
+ source,
160
+ sourceIndex,
161
+ epsilon: PcbScene3dCopperFillCoverageContext.#GEOMETRY_EPSILON,
162
+ pointRepresentation: 'numeric'
163
+ })
164
+ }
165
+
166
+ /**
167
+ * Returns whether an optional source-index collection permits one area.
168
+ * @param {number} sourceIndex Candidate source position.
169
+ * @param {Set<number> | number[] | null} allowedSourceIndexes Optional allow set.
170
+ * @returns {boolean}
171
+ */
172
+ static #isAllowedSourceIndex(sourceIndex, allowedSourceIndexes) {
173
+ if (allowedSourceIndexes === null) {
174
+ return true
175
+ }
176
+
177
+ if (typeof allowedSourceIndexes?.has === 'function') {
178
+ return allowedSourceIndexes.has(sourceIndex)
179
+ }
180
+
181
+ return (
182
+ Array.isArray(allowedSourceIndexes) &&
183
+ allowedSourceIndexes.includes(sourceIndex)
184
+ )
185
+ }
186
+
187
+ /**
188
+ * Resolves area bounds for the AABB index.
189
+ * @param {{ bounds: object }} area Prepared area.
190
+ * @returns {object}
191
+ */
192
+ static #resolveBounds(area) {
193
+ return area.bounds
194
+ }
195
+
196
+ /**
197
+ * Resolves stable flattened fill-island order for the AABB index.
198
+ * @param {{ sourceIndex: number }} area Prepared area.
199
+ * @returns {number}
200
+ */
201
+ static #resolveSourceIndex(area) {
202
+ return area.sourceIndex
203
+ }
204
+ }
@@ -0,0 +1,192 @@
1
+ import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
2
+
3
+ /**
4
+ * Resolves copper fills into canonical side-local polygon loop sets.
5
+ */
6
+ export class PcbScene3dCopperFillLoopSetResolver {
7
+ static #AREA_EPSILON = 0.001
8
+ static #GEOMETRY_EPSILON = 0.001
9
+
10
+ /**
11
+ * Resolves every valid fill island in source order.
12
+ * @param {object[]} fills Filled copper primitives.
13
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
14
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
15
+ * @returns {{ outer: number[][], holes: number[][][], bounds: { minX: number, minY: number, maxX: number, maxY: number } }[]}
16
+ */
17
+ static resolve(fills, normalizeBoardPoint, mirrorY) {
18
+ const loopSets = []
19
+
20
+ for (const fill of fills || []) {
21
+ for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(
22
+ fill
23
+ )) {
24
+ const loopSet =
25
+ PcbScene3dCopperFillLoopSetResolver.#normalizeLoopSet(
26
+ loops,
27
+ normalizeBoardPoint,
28
+ mirrorY
29
+ )
30
+ if (loopSet) {
31
+ loopSets.push(loopSet)
32
+ }
33
+ }
34
+ }
35
+
36
+ return loopSets
37
+ }
38
+
39
+ /**
40
+ * Normalizes one fill island and discards degenerate outer geometry.
41
+ * @param {{ outer?: any[], holes?: any[][] }} loops Source loops.
42
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
43
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
44
+ * @returns {{ outer: number[][], holes: number[][][], bounds: { minX: number, minY: number, maxX: number, maxY: number } } | null}
45
+ */
46
+ static #normalizeLoopSet(loops, normalizeBoardPoint, mirrorY) {
47
+ const outer = PcbScene3dCopperFillLoopSetResolver.#normalizeLoop(
48
+ loops?.outer,
49
+ normalizeBoardPoint,
50
+ mirrorY
51
+ )
52
+ if (!PcbScene3dCopperFillLoopSetResolver.#isValidLoop(outer)) {
53
+ return null
54
+ }
55
+
56
+ const holes = []
57
+ for (const sourceHole of loops?.holes || []) {
58
+ const hole = PcbScene3dCopperFillLoopSetResolver.#normalizeLoop(
59
+ sourceHole,
60
+ normalizeBoardPoint,
61
+ mirrorY
62
+ )
63
+ if (PcbScene3dCopperFillLoopSetResolver.#isValidLoop(hole)) {
64
+ holes.push(hole)
65
+ }
66
+ }
67
+
68
+ return {
69
+ outer,
70
+ holes,
71
+ bounds: PcbScene3dCopperFillLoopSetResolver.#resolveBounds(outer)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Converts one loop into clean finite side-local coordinate pairs.
77
+ * @param {any[]} loop Source points.
78
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
79
+ * @param {boolean} mirrorY Whether to mirror underside Y coordinates.
80
+ * @returns {number[][]}
81
+ */
82
+ static #normalizeLoop(loop, normalizeBoardPoint, mirrorY) {
83
+ const points = []
84
+
85
+ for (const point of loop || []) {
86
+ const normalized = normalizeBoardPoint(
87
+ Number(point?.x ?? point?.[0]),
88
+ Number(point?.y ?? point?.[1])
89
+ )
90
+ const x = Number(normalized?.x)
91
+ const y = mirrorY ? -Number(normalized?.y) : Number(normalized?.y)
92
+
93
+ if (Number.isFinite(x) && Number.isFinite(y)) {
94
+ points.push([x, y])
95
+ }
96
+ }
97
+
98
+ return PcbScene3dCopperFillLoopSetResolver.#cleanLoop(points)
99
+ }
100
+
101
+ /**
102
+ * Removes consecutive duplicate and explicit closing points.
103
+ * @param {number[][]} points Candidate points.
104
+ * @returns {number[][]}
105
+ */
106
+ static #cleanLoop(points) {
107
+ const loop = []
108
+
109
+ for (const point of points || []) {
110
+ const previous = loop[loop.length - 1]
111
+ if (
112
+ previous &&
113
+ Math.abs(previous[0] - point[0]) <
114
+ PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON &&
115
+ Math.abs(previous[1] - point[1]) <
116
+ PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON
117
+ ) {
118
+ continue
119
+ }
120
+ loop.push(point)
121
+ }
122
+
123
+ const first = loop[0]
124
+ const last = loop[loop.length - 1]
125
+ if (
126
+ first &&
127
+ last &&
128
+ Math.abs(first[0] - last[0]) <
129
+ PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON &&
130
+ Math.abs(first[1] - last[1]) <
131
+ PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON
132
+ ) {
133
+ loop.pop()
134
+ }
135
+
136
+ return loop
137
+ }
138
+
139
+ /**
140
+ * Returns true when one loop has sufficient non-collinear area.
141
+ * @param {number[][]} loop Candidate loop.
142
+ * @returns {boolean}
143
+ */
144
+ static #isValidLoop(loop) {
145
+ return (
146
+ Array.isArray(loop) &&
147
+ loop.length >= 3 &&
148
+ Math.abs(PcbScene3dCopperFillLoopSetResolver.#signedArea(loop)) >
149
+ PcbScene3dCopperFillLoopSetResolver.#AREA_EPSILON
150
+ )
151
+ }
152
+
153
+ /**
154
+ * Computes one loop's signed shoelace area.
155
+ * @param {number[][]} loop Candidate loop.
156
+ * @returns {number}
157
+ */
158
+ static #signedArea(loop) {
159
+ let area = 0
160
+
161
+ for (let index = 0; index < loop.length; index += 1) {
162
+ const current = loop[index]
163
+ const next = loop[(index + 1) % loop.length]
164
+ area += current[0] * next[1] - next[0] * current[1]
165
+ }
166
+
167
+ return area / 2
168
+ }
169
+
170
+ /**
171
+ * Resolves finite axis-aligned bounds for a valid loop.
172
+ * @param {number[][]} loop Candidate loop.
173
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
174
+ */
175
+ static #resolveBounds(loop) {
176
+ const bounds = {
177
+ minX: Infinity,
178
+ minY: Infinity,
179
+ maxX: -Infinity,
180
+ maxY: -Infinity
181
+ }
182
+
183
+ for (const point of loop) {
184
+ bounds.minX = Math.min(bounds.minX, point[0])
185
+ bounds.minY = Math.min(bounds.minY, point[1])
186
+ bounds.maxX = Math.max(bounds.maxX, point[0])
187
+ bounds.maxY = Math.max(bounds.maxY, point[1])
188
+ }
189
+
190
+ return bounds
191
+ }
192
+ }