pcb-scene3d-viewer 1.1.22 → 1.1.44

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 (67) hide show
  1. package/AGENTS.md +4 -3
  2. package/README.md +3 -2
  3. package/docs/api.md +59 -1
  4. package/docs/circuitjson.md +206 -22
  5. package/docs/model-format.md +65 -3
  6. package/package.json +2 -2
  7. package/spec/library-scope.md +2 -2
  8. package/src/CircuitJsonCadModelAssetResolver.mjs +261 -0
  9. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +49 -10
  10. package/src/PcbAssemblyComponentMeshBuilder.mjs +842 -0
  11. package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
  12. package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
  13. package/src/PcbAssemblyGeometryBuildProgress.mjs +3 -0
  14. package/src/PcbAssemblyGeometryBuilder.mjs +94 -174
  15. package/src/PcbAssemblyGltfModelMeshParser.mjs +969 -0
  16. package/src/PcbAssemblyGltfValidator.mjs +460 -0
  17. package/src/PcbAssemblyGltfWriter.mjs +978 -0
  18. package/src/PcbAssemblyModelMeshLoader.mjs +330 -2
  19. package/src/PcbAssemblyPadMeshBuilder.mjs +9 -7
  20. package/src/PcbAssemblyTextModelMeshParser.mjs +725 -0
  21. package/src/PcbModelArchiveExporter.mjs +17 -2
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +585 -246
  23. package/src/PcbScene3dCircuitJsonCopperPourBuilder.mjs +298 -0
  24. package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +606 -0
  25. package/src/PcbScene3dCircuitJsonGeometry.mjs +542 -0
  26. package/src/PcbScene3dCircuitJsonLayer.mjs +110 -0
  27. package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
  28. package/src/PcbScene3dCircuitJsonModelUrlResolver.mjs +142 -0
  29. package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +729 -0
  30. package/src/PcbScene3dCircuitJsonSolderPasteBuilder.mjs +359 -0
  31. package/src/PcbScene3dCircuitJsonThermalSpokeBuilder.mjs +167 -0
  32. package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +335 -0
  33. package/src/PcbScene3dComponentVisibility.mjs +9 -6
  34. package/src/PcbScene3dController.mjs +25 -55
  35. package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
  36. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
  37. package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
  38. package/src/PcbScene3dCopperFactory.mjs +99 -85
  39. package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
  40. package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
  41. package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
  42. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
  43. package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
  44. package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
  45. package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
  46. package/src/PcbScene3dExternalModels.mjs +10 -7
  47. package/src/PcbScene3dFallbackBodyFactory.mjs +45 -4
  48. package/src/PcbScene3dFallbackVisibility.mjs +17 -32
  49. package/src/PcbScene3dFootprintBodyBuilder.mjs +894 -0
  50. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
  51. package/src/PcbScene3dModelSeatingPolicy.mjs +58 -0
  52. package/src/PcbScene3dPadFactory.mjs +35 -2
  53. package/src/PcbScene3dRenderGroupVisibility.mjs +10 -13
  54. package/src/PcbScene3dRuntime.mjs +99 -91
  55. package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
  56. package/src/PcbScene3dRuntimeSurfaceArtworkLoader.mjs +91 -0
  57. package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
  58. package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
  59. package/src/PcbScene3dSelectionMarkerFactory.mjs +368 -0
  60. package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
  61. package/src/PcbScene3dSelectionStyler.mjs +52 -2
  62. package/src/PcbScene3dStaticBodyFactory.mjs +30 -8
  63. package/src/PcbScene3dStepLoader.mjs +42 -6
  64. package/src/PcbScene3dText.mjs +1 -0
  65. package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
  66. package/src/PcbScene3dViaFactory.mjs +27 -7
  67. package/src/scene3d.mjs +4 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Bridges CAD component model asset metadata to scene model resolution.
3
+ */
4
+ export class CircuitJsonCadModelAssetResolver {
5
+ /**
6
+ * Adds URL fields derived from CAD `model_asset` records.
7
+ * @param {object | object[]} documentModel Document model.
8
+ * @returns {object | object[]}
9
+ */
10
+ static withModelAssetUrls(documentModel) {
11
+ const elements =
12
+ CircuitJsonCadModelAssetResolver.#elements(documentModel)
13
+ if (
14
+ !elements.some((element) =>
15
+ CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
16
+ )
17
+ ) {
18
+ return documentModel
19
+ }
20
+
21
+ const nextElements = elements.map((element) =>
22
+ CircuitJsonCadModelAssetResolver.#withModelAssetUrl(element)
23
+ )
24
+
25
+ if (Array.isArray(documentModel)) {
26
+ Object.assign(nextElements, {
27
+ sourceFormat: documentModel.sourceFormat,
28
+ kind: documentModel.kind,
29
+ fileName: documentModel.fileName,
30
+ bom: documentModel.bom
31
+ })
32
+ return nextElements
33
+ }
34
+
35
+ if (Array.isArray(documentModel?.elements)) {
36
+ return { ...documentModel, elements: nextElements }
37
+ }
38
+ if (Array.isArray(documentModel?.circuitJson)) {
39
+ return { ...documentModel, circuitJson: nextElements }
40
+ }
41
+ return documentModel
42
+ }
43
+
44
+ /**
45
+ * Adds a session-asset-aware model URL resolver.
46
+ * @param {object} options Scene preparation options.
47
+ * @returns {object}
48
+ */
49
+ static withSessionAssetResolver(options = {}) {
50
+ const sessionAssets = Array.isArray(options.sessionAssets)
51
+ ? options.sessionAssets
52
+ : []
53
+ const callerResolver =
54
+ typeof options.modelUrlResolver === 'function'
55
+ ? options.modelUrlResolver
56
+ : null
57
+
58
+ return {
59
+ ...options,
60
+ modelUrlResolver: (url, context) =>
61
+ CircuitJsonCadModelAssetResolver.#resolveModelUrl(
62
+ url,
63
+ context,
64
+ sessionAssets,
65
+ callerResolver
66
+ )
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Adds a model URL field to one CAD component when possible.
72
+ * @param {object} element Element row.
73
+ * @returns {object}
74
+ */
75
+ static #withModelAssetUrl(element) {
76
+ const field =
77
+ CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
78
+ if (!field) return element
79
+ const asset = element.model_asset || {}
80
+ const sourceUrl = String(
81
+ asset.project_relative_path || asset.url || ''
82
+ ).trim()
83
+ if (!sourceUrl) return element
84
+ return { ...element, [field]: sourceUrl }
85
+ }
86
+
87
+ /**
88
+ * Resolves a model URL field for one CAD component asset.
89
+ * @param {object} element Element row.
90
+ * @returns {string}
91
+ */
92
+ static #modelAssetUrlField(element) {
93
+ if (element?.type !== 'cad_component') return ''
94
+ const asset = element.model_asset
95
+ if (!asset || typeof asset !== 'object') return ''
96
+ if (CircuitJsonCadModelAssetResolver.#hasModelUrl(element)) return ''
97
+ const format = CircuitJsonCadModelAssetResolver.#format(asset)
98
+ return (
99
+ {
100
+ '3mf': 'model_3mf_url',
101
+ glb: 'model_glb_url',
102
+ gltf: 'model_gltf_url',
103
+ obj: 'model_obj_url',
104
+ step: 'model_step_url',
105
+ stl: 'model_stl_url',
106
+ wrl: 'model_wrl_url'
107
+ }[format] || ''
108
+ )
109
+ }
110
+
111
+ /**
112
+ * Resolves a URL to a session asset or caller-provided value.
113
+ * @param {string} url Model URL.
114
+ * @param {object} context Resolver context.
115
+ * @param {object[]} sessionAssets Session asset rows.
116
+ * @param {((url: string, context: object) => any) | null} callerResolver Caller resolver.
117
+ * @returns {object | string | null}
118
+ */
119
+ static #resolveModelUrl(url, context, sessionAssets, callerResolver) {
120
+ const matchedAsset =
121
+ CircuitJsonCadModelAssetResolver.#matchingSessionAsset(
122
+ url,
123
+ context,
124
+ sessionAssets
125
+ )
126
+ if (matchedAsset) {
127
+ return matchedAsset
128
+ }
129
+ return callerResolver ? callerResolver(url, context) : null
130
+ }
131
+
132
+ /**
133
+ * Finds a session asset matching one CAD model URL.
134
+ * @param {string} url Model URL.
135
+ * @param {object} context Resolver context.
136
+ * @param {object[]} sessionAssets Session assets.
137
+ * @returns {object | null}
138
+ */
139
+ static #matchingSessionAsset(url, context, sessionAssets) {
140
+ const candidates = CircuitJsonCadModelAssetResolver.#urlCandidates(
141
+ url,
142
+ context
143
+ )
144
+ return (
145
+ sessionAssets.find((asset) =>
146
+ CircuitJsonCadModelAssetResolver.#assetMatches(
147
+ asset,
148
+ candidates
149
+ )
150
+ ) || null
151
+ )
152
+ }
153
+
154
+ /**
155
+ * Builds normalized URL/path candidates for matching.
156
+ * @param {string} url Model URL.
157
+ * @param {object} context Resolver context.
158
+ * @returns {Set<string>}
159
+ */
160
+ static #urlCandidates(url, context) {
161
+ return new Set(
162
+ [
163
+ url,
164
+ context?.cadComponent?.model_asset?.project_relative_path,
165
+ context?.cadComponent?.model_asset?.url
166
+ ]
167
+ .map((value) => CircuitJsonCadModelAssetResolver.#key(value))
168
+ .filter(Boolean)
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Returns true when a session asset matches a model URL.
174
+ * @param {object} asset Session asset.
175
+ * @param {Set<string>} candidates URL candidates.
176
+ * @returns {boolean}
177
+ */
178
+ static #assetMatches(asset, candidates) {
179
+ return [
180
+ asset?.relativePath,
181
+ asset?.sourceUrl,
182
+ asset?.url,
183
+ asset?.name
184
+ ].some((value) =>
185
+ candidates.has(CircuitJsonCadModelAssetResolver.#key(value))
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Resolves a supported model format from asset metadata.
191
+ * @param {object} asset Model asset metadata.
192
+ * @returns {string}
193
+ */
194
+ static #format(asset) {
195
+ const text = String(
196
+ asset.format ||
197
+ asset.mimetype ||
198
+ asset.project_relative_path ||
199
+ asset.url ||
200
+ ''
201
+ ).toLowerCase()
202
+ if (/\bglb\b|\.glb(?:[?#]|$)/u.test(text)) return 'glb'
203
+ if (/\bgltf\b|\.gltf(?:[?#]|$)/u.test(text)) return 'gltf'
204
+ if (/\b3mf\b|\.3mf(?:[?#]|$)/u.test(text)) return '3mf'
205
+ if (/\bobj\b|\.obj(?:[?#]|$)/u.test(text)) return 'obj'
206
+ if (/\bstl\b|\.stl(?:[?#]|$)/u.test(text)) return 'stl'
207
+ if (/\bwrl\b|vrml|\.wrl(?:[?#]|$)/u.test(text)) return 'wrl'
208
+ if (/step|stp|\.step(?:[?#]|$)|\.stp(?:[?#]|$)/u.test(text)) {
209
+ return 'step'
210
+ }
211
+ return ''
212
+ }
213
+
214
+ /**
215
+ * Returns true when a CAD component already has a model URL.
216
+ * @param {object} element Element row.
217
+ * @returns {boolean}
218
+ */
219
+ static #hasModelUrl(element) {
220
+ return [
221
+ 'model_step_url',
222
+ 'model_stp_url',
223
+ 'model_3mf_url',
224
+ 'model_wrl_url',
225
+ 'model_vrml_url',
226
+ 'model_glb_url',
227
+ 'model_gltf_url',
228
+ 'model_stl_url',
229
+ 'model_obj_url'
230
+ ].some((field) => String(element?.[field] || '').trim())
231
+ }
232
+
233
+ /**
234
+ * Reads element rows from document wrappers.
235
+ * @param {object | object[]} documentModel Document model.
236
+ * @returns {object[]}
237
+ */
238
+ static #elements(documentModel) {
239
+ if (Array.isArray(documentModel)) return documentModel
240
+ if (Array.isArray(documentModel?.elements))
241
+ return documentModel.elements
242
+ if (Array.isArray(documentModel?.circuitJson)) {
243
+ return documentModel.circuitJson
244
+ }
245
+ return []
246
+ }
247
+
248
+ /**
249
+ * Normalizes a path or URL key.
250
+ * @param {unknown} value Raw path.
251
+ * @returns {string}
252
+ */
253
+ static #key(value) {
254
+ return String(value || '')
255
+ .trim()
256
+ .split(/[?#]/u)[0]
257
+ .replace(/^https?:\/\/[^/]+\//iu, '')
258
+ .replace(/^\/+/u, '')
259
+ .toLowerCase()
260
+ }
261
+ }
@@ -1,9 +1,14 @@
1
1
  import earcut from 'earcut'
2
2
  import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
3
3
 
4
- const MAX_HOLE_POINTS = 48
4
+ const MAX_HOLE_POINTS = 64
5
5
  const MIN_HOLE_POINTS = 8
6
6
  const GEOMETRY_EPSILON = 0.001
7
+ const DRILL_QUALITY_POINTS = {
8
+ low: 16,
9
+ medium: 24,
10
+ high: 48
11
+ }
7
12
 
8
13
  /**
9
14
  * Builds PCB substrate meshes, including through-board drill cutouts.
@@ -145,15 +150,20 @@ export class PcbAssemblyBoardSubstrateBuilder {
145
150
  * @returns {number[][][]}
146
151
  */
147
152
  static #boardHoleLoops(sceneDescription, outer) {
153
+ const explicitLoops =
154
+ PcbAssemblyBoardSubstrateBuilder.#explicitCutoutLoops(
155
+ sceneDescription
156
+ )
148
157
  const primitiveLoops =
149
158
  PcbAssemblyBoardSubstrateBuilder.#primitiveHoleLoops(
150
159
  sceneDescription
151
160
  )
152
- const rawLoops = primitiveLoops.length
161
+ const fallbackLoops = primitiveLoops.length
153
162
  ? primitiveLoops
154
163
  : PcbAssemblyBoardSubstrateBuilder.#drillCutoutLoops(
155
164
  sceneDescription
156
165
  )
166
+ const rawLoops = [...explicitLoops, ...fallbackLoops]
157
167
  const seen = new Set()
158
168
  const holes = []
159
169
 
@@ -189,6 +199,19 @@ export class PcbAssemblyBoardSubstrateBuilder {
189
199
  return holes
190
200
  }
191
201
 
202
+ /**
203
+ * Reads explicit board cutout contours from the scene board metadata.
204
+ * @param {object} sceneDescription Prepared scene description.
205
+ * @returns {Array<Array<object | number[]>>}
206
+ */
207
+ static #explicitCutoutLoops(sceneDescription) {
208
+ return PcbAssemblyBoardSubstrateBuilder.#array(
209
+ sceneDescription?.board?.cutouts
210
+ )
211
+ .map((cutout) => cutout?.points || cutout?.vertices || cutout)
212
+ .filter((loop) => Array.isArray(loop))
213
+ }
214
+
192
215
  /**
193
216
  * Reads existing drill cutout contours from scene silkscreen details.
194
217
  * @param {object} sceneDescription Prepared scene description.
@@ -210,12 +233,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
210
233
  */
211
234
  static #primitiveHoleLoops(sceneDescription) {
212
235
  const detail = sceneDescription?.detail || {}
236
+ const pointCount =
237
+ PcbAssemblyBoardSubstrateBuilder.#drillQualityPointCount(
238
+ detail?.drillQuality
239
+ )
213
240
  return [
214
241
  ...PcbAssemblyBoardSubstrateBuilder.#array(detail.pads).map((pad) =>
215
- PcbAssemblyBoardSubstrateBuilder.#padHoleLoop(pad)
242
+ PcbAssemblyBoardSubstrateBuilder.#padHoleLoop(pad, pointCount)
216
243
  ),
217
244
  ...PcbAssemblyBoardSubstrateBuilder.#array(detail.vias).map((via) =>
218
- PcbAssemblyBoardSubstrateBuilder.#viaHoleLoop(via)
245
+ PcbAssemblyBoardSubstrateBuilder.#viaHoleLoop(via, pointCount)
219
246
  )
220
247
  ].filter(Boolean)
221
248
  }
@@ -223,9 +250,10 @@ export class PcbAssemblyBoardSubstrateBuilder {
223
250
  /**
224
251
  * Builds a fallback pad drill loop.
225
252
  * @param {object} pad Pad primitive.
253
+ * @param {number} pointCount Circular drill point count.
226
254
  * @returns {number[][] | null}
227
255
  */
228
- static #padHoleLoop(pad) {
256
+ static #padHoleLoop(pad, pointCount) {
229
257
  const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
230
258
  pad?.holeDiameter,
231
259
  pad?.drillDiameter,
@@ -238,8 +266,8 @@ export class PcbAssemblyBoardSubstrateBuilder {
238
266
  return null
239
267
  }
240
268
 
241
- const x = Number(pad?.x)
242
- const y = Number(pad?.y)
269
+ const x = Number(pad?.x) + Number(pad?.holeOffsetX || 0)
270
+ const y = Number(pad?.y) + Number(pad?.holeOffsetY || 0)
243
271
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
244
272
  return null
245
273
  }
@@ -265,15 +293,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
265
293
  )
266
294
  }
267
295
 
268
- return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, 24)
296
+ return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, pointCount)
269
297
  }
270
298
 
271
299
  /**
272
300
  * Builds a fallback via drill loop.
273
301
  * @param {object} via Via primitive.
302
+ * @param {number} pointCount Circular drill point count.
274
303
  * @returns {number[][] | null}
275
304
  */
276
- static #viaHoleLoop(via) {
305
+ static #viaHoleLoop(via, pointCount) {
277
306
  const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
278
307
  via?.holeDiameter,
279
308
  via?.drillDiameter,
@@ -284,7 +313,7 @@ export class PcbAssemblyBoardSubstrateBuilder {
284
313
  const y = Number(via?.y)
285
314
 
286
315
  return diameter && Number.isFinite(x) && Number.isFinite(y)
287
- ? PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, 24)
316
+ ? PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, pointCount)
288
317
  : null
289
318
  }
290
319
 
@@ -458,6 +487,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
458
487
  return 0
459
488
  }
460
489
 
490
+ /**
491
+ * Resolves circular drill sampling from scene detail quality metadata.
492
+ * @param {unknown} value Candidate quality.
493
+ * @returns {number}
494
+ */
495
+ static #drillQualityPointCount(value) {
496
+ const quality = String(value || 'medium').toLowerCase()
497
+ return DRILL_QUALITY_POINTS[quality] || DRILL_QUALITY_POINTS.medium
498
+ }
499
+
461
500
  /**
462
501
  * Normalizes a value to an array.
463
502
  * @param {unknown} value Candidate value.