pcb-scene3d-viewer 1.1.21 → 1.1.31

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 (55) hide show
  1. package/README.md +3 -2
  2. package/docs/api.md +27 -1
  3. package/docs/circuitjson.md +63 -18
  4. package/docs/model-format.md +13 -2
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +2 -2
  7. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
  8. package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
  9. package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
  10. package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
  12. package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
  13. package/src/PcbAssemblyGltfValidator.mjs +460 -0
  14. package/src/PcbAssemblyGltfWriter.mjs +801 -0
  15. package/src/PcbAssemblyMeshUtils.mjs +117 -0
  16. package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
  17. package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
  18. package/src/PcbAssemblyStepWriter.mjs +24 -2
  19. package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
  20. package/src/PcbModelArchiveExporter.mjs +521 -7
  21. package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
  22. package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
  23. package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
  24. package/src/PcbScene3dComponentVisibility.mjs +100 -5
  25. package/src/PcbScene3dController.mjs +25 -55
  26. package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
  27. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
  28. package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
  29. package/src/PcbScene3dCopperFactory.mjs +99 -85
  30. package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
  31. package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
  33. package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
  34. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
  35. package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
  36. package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
  37. package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
  38. package/src/PcbScene3dExternalModels.mjs +16 -7
  39. package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
  40. package/src/PcbScene3dFallbackVisibility.mjs +58 -1
  41. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
  42. package/src/PcbScene3dPadFactory.mjs +35 -2
  43. package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
  44. package/src/PcbScene3dRuntime.mjs +94 -100
  45. package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
  46. package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
  47. package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
  48. package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
  49. package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
  50. package/src/PcbScene3dSelectionStyler.mjs +52 -2
  51. package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
  52. package/src/PcbScene3dText.mjs +1 -0
  53. package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
  54. package/src/PcbScene3dViaFactory.mjs +27 -7
  55. package/src/scene3d.mjs +3 -0
@@ -0,0 +1,460 @@
1
+ import { PcbAssemblyGltfModelMeshParser } from './PcbAssemblyGltfModelMeshParser.mjs'
2
+
3
+ /**
4
+ * Performs structural validation for generated GLTF and GLB assembly payloads.
5
+ */
6
+ export class PcbAssemblyGltfValidator {
7
+ /**
8
+ * Validates a GLTF JSON object, GLTF JSON string, or GLB byte payload.
9
+ * @param {object | string | Uint8Array} payload GLTF or GLB payload.
10
+ * @returns {{ severity: 'error', code: string, message: string }[]}
11
+ */
12
+ static validate(payload) {
13
+ const issues = []
14
+
15
+ try {
16
+ const parsed = PcbAssemblyGltfValidator.#parsePayload(payload)
17
+ PcbAssemblyGltfValidator.#validateDocument(parsed, issues)
18
+ } catch (error) {
19
+ issues.push(
20
+ PcbAssemblyGltfValidator.#issue(
21
+ 'gltf_parse_failed',
22
+ String(error?.message || error || 'Invalid GLTF payload.')
23
+ )
24
+ )
25
+ }
26
+
27
+ return issues
28
+ }
29
+
30
+ /**
31
+ * Parses a supported validation payload.
32
+ * @param {object | string | Uint8Array} payload GLTF or GLB payload.
33
+ * @returns {{ gltf: object, binaryBuffer: Uint8Array | null }}
34
+ */
35
+ static #parsePayload(payload) {
36
+ if (payload instanceof Uint8Array) {
37
+ return PcbAssemblyGltfModelMeshParser.parseGlb(payload)
38
+ }
39
+
40
+ if (typeof payload === 'string') {
41
+ return { gltf: JSON.parse(payload), binaryBuffer: null }
42
+ }
43
+
44
+ if (payload && typeof payload === 'object') {
45
+ return { gltf: payload, binaryBuffer: null }
46
+ }
47
+
48
+ throw new Error('Unsupported GLTF validation payload.')
49
+ }
50
+
51
+ /**
52
+ * Validates the main GLTF document graph.
53
+ * @param {{ gltf: object, binaryBuffer: Uint8Array | null }} parsed Parsed payload.
54
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
55
+ * @returns {void}
56
+ */
57
+ static #validateDocument(parsed, issues) {
58
+ const gltf = parsed.gltf || {}
59
+ if (String(gltf?.asset?.version || '') !== '2.0') {
60
+ issues.push(
61
+ PcbAssemblyGltfValidator.#issue(
62
+ 'gltf_asset_version_invalid',
63
+ 'GLTF asset version must be 2.0.'
64
+ )
65
+ )
66
+ }
67
+
68
+ PcbAssemblyGltfValidator.#validateScenes(gltf, issues)
69
+ PcbAssemblyGltfValidator.#validateNodes(gltf, issues)
70
+ PcbAssemblyGltfValidator.#validateMeshes(gltf, issues)
71
+ PcbAssemblyGltfValidator.#validateBufferViews(
72
+ gltf,
73
+ parsed.binaryBuffer,
74
+ issues
75
+ )
76
+ PcbAssemblyGltfValidator.#validateAccessors(gltf, issues)
77
+ PcbAssemblyGltfValidator.#validateTextures(gltf, issues)
78
+ }
79
+
80
+ /**
81
+ * Validates scenes and root node references.
82
+ * @param {object} gltf GLTF JSON.
83
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
84
+ * @returns {void}
85
+ */
86
+ static #validateScenes(gltf, issues) {
87
+ const scenes = PcbAssemblyGltfValidator.#array(gltf?.scenes)
88
+ if (!scenes.length) {
89
+ issues.push(
90
+ PcbAssemblyGltfValidator.#issue(
91
+ 'gltf_scene_missing',
92
+ 'GLTF document must include at least one scene.'
93
+ )
94
+ )
95
+ return
96
+ }
97
+
98
+ scenes.forEach((scene, sceneIndex) => {
99
+ PcbAssemblyGltfValidator.#array(scene?.nodes).forEach(
100
+ (nodeIndex) => {
101
+ if (
102
+ !PcbAssemblyGltfValidator.#hasIndex(
103
+ gltf?.nodes,
104
+ nodeIndex
105
+ )
106
+ ) {
107
+ issues.push(
108
+ PcbAssemblyGltfValidator.#issue(
109
+ 'gltf_scene_node_invalid',
110
+ 'Scene ' +
111
+ sceneIndex +
112
+ ' references a missing node.'
113
+ )
114
+ )
115
+ }
116
+ }
117
+ )
118
+ })
119
+ }
120
+
121
+ /**
122
+ * Validates node mesh and child references.
123
+ * @param {object} gltf GLTF JSON.
124
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
125
+ * @returns {void}
126
+ */
127
+ static #validateNodes(gltf, issues) {
128
+ PcbAssemblyGltfValidator.#array(gltf?.nodes).forEach(
129
+ (node, nodeIndex) => {
130
+ if (
131
+ Number.isInteger(node?.mesh) &&
132
+ !PcbAssemblyGltfValidator.#hasIndex(gltf?.meshes, node.mesh)
133
+ ) {
134
+ issues.push(
135
+ PcbAssemblyGltfValidator.#issue(
136
+ 'gltf_node_mesh_invalid',
137
+ 'Node ' + nodeIndex + ' references a missing mesh.'
138
+ )
139
+ )
140
+ }
141
+
142
+ PcbAssemblyGltfValidator.#array(node?.children).forEach(
143
+ (childIndex) => {
144
+ if (
145
+ !PcbAssemblyGltfValidator.#hasIndex(
146
+ gltf?.nodes,
147
+ childIndex
148
+ )
149
+ ) {
150
+ issues.push(
151
+ PcbAssemblyGltfValidator.#issue(
152
+ 'gltf_node_child_invalid',
153
+ 'Node ' +
154
+ nodeIndex +
155
+ ' references a missing child node.'
156
+ )
157
+ )
158
+ }
159
+ }
160
+ )
161
+ }
162
+ )
163
+ }
164
+
165
+ /**
166
+ * Validates mesh primitive references.
167
+ * @param {object} gltf GLTF JSON.
168
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
169
+ * @returns {void}
170
+ */
171
+ static #validateMeshes(gltf, issues) {
172
+ PcbAssemblyGltfValidator.#array(gltf?.meshes).forEach(
173
+ (mesh, meshIndex) => {
174
+ PcbAssemblyGltfValidator.#array(mesh?.primitives).forEach(
175
+ (primitive, primitiveIndex) => {
176
+ PcbAssemblyGltfValidator.#validatePrimitive(
177
+ gltf,
178
+ meshIndex,
179
+ primitiveIndex,
180
+ primitive,
181
+ issues
182
+ )
183
+ }
184
+ )
185
+ }
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Validates one primitive.
191
+ * @param {object} gltf GLTF JSON.
192
+ * @param {number} meshIndex Mesh index.
193
+ * @param {number} primitiveIndex Primitive index.
194
+ * @param {object} primitive Primitive JSON.
195
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
196
+ * @returns {void}
197
+ */
198
+ static #validatePrimitive(
199
+ gltf,
200
+ meshIndex,
201
+ primitiveIndex,
202
+ primitive,
203
+ issues
204
+ ) {
205
+ const positionAccessor = primitive?.attributes?.POSITION
206
+ if (
207
+ !Number.isInteger(positionAccessor) ||
208
+ !PcbAssemblyGltfValidator.#hasIndex(
209
+ gltf?.accessors,
210
+ positionAccessor
211
+ )
212
+ ) {
213
+ issues.push(
214
+ PcbAssemblyGltfValidator.#issue(
215
+ 'gltf_primitive_position_invalid',
216
+ 'Mesh ' +
217
+ meshIndex +
218
+ ' primitive ' +
219
+ primitiveIndex +
220
+ ' must reference a POSITION accessor.'
221
+ )
222
+ )
223
+ }
224
+
225
+ if (
226
+ Number.isInteger(primitive?.indices) &&
227
+ !PcbAssemblyGltfValidator.#hasIndex(
228
+ gltf?.accessors,
229
+ primitive.indices
230
+ )
231
+ ) {
232
+ issues.push(
233
+ PcbAssemblyGltfValidator.#issue(
234
+ 'gltf_primitive_indices_invalid',
235
+ 'Mesh ' +
236
+ meshIndex +
237
+ ' primitive ' +
238
+ primitiveIndex +
239
+ ' references a missing index accessor.'
240
+ )
241
+ )
242
+ }
243
+
244
+ if (
245
+ Number.isInteger(primitive?.material) &&
246
+ !PcbAssemblyGltfValidator.#hasIndex(
247
+ gltf?.materials,
248
+ primitive.material
249
+ )
250
+ ) {
251
+ issues.push(
252
+ PcbAssemblyGltfValidator.#issue(
253
+ 'gltf_primitive_material_invalid',
254
+ 'Mesh ' +
255
+ meshIndex +
256
+ ' primitive ' +
257
+ primitiveIndex +
258
+ ' references a missing material.'
259
+ )
260
+ )
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Validates buffer view ranges.
266
+ * @param {object} gltf GLTF JSON.
267
+ * @param {Uint8Array | null} binaryBuffer GLB binary chunk.
268
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
269
+ * @returns {void}
270
+ */
271
+ static #validateBufferViews(gltf, binaryBuffer, issues) {
272
+ const buffers = PcbAssemblyGltfValidator.#array(gltf?.buffers)
273
+ PcbAssemblyGltfValidator.#array(gltf?.bufferViews).forEach(
274
+ (bufferView, index) => {
275
+ const bufferIndex = Number(bufferView?.buffer || 0)
276
+ const buffer = buffers[bufferIndex]
277
+ if (!buffer) {
278
+ issues.push(
279
+ PcbAssemblyGltfValidator.#issue(
280
+ 'gltf_buffer_view_buffer_invalid',
281
+ 'Buffer view ' +
282
+ index +
283
+ ' references a missing buffer.'
284
+ )
285
+ )
286
+ return
287
+ }
288
+
289
+ const bufferLength =
290
+ Number(buffer.byteLength || 0) ||
291
+ PcbAssemblyGltfValidator.#bufferUriLength(buffer?.uri) ||
292
+ Number(binaryBuffer?.byteLength || 0)
293
+ const end =
294
+ Number(bufferView.byteOffset || 0) +
295
+ Number(bufferView.byteLength || 0)
296
+ if (end > bufferLength) {
297
+ issues.push(
298
+ PcbAssemblyGltfValidator.#issue(
299
+ 'gltf_buffer_view_range_invalid',
300
+ 'Buffer view ' + index + ' exceeds its buffer.'
301
+ )
302
+ )
303
+ }
304
+ }
305
+ )
306
+ }
307
+
308
+ /**
309
+ * Validates accessor buffer-view references.
310
+ * @param {object} gltf GLTF JSON.
311
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
312
+ * @returns {void}
313
+ */
314
+ static #validateAccessors(gltf, issues) {
315
+ PcbAssemblyGltfValidator.#array(gltf?.accessors).forEach(
316
+ (accessor, index) => {
317
+ if (
318
+ !PcbAssemblyGltfValidator.#hasIndex(
319
+ gltf?.bufferViews,
320
+ accessor?.bufferView
321
+ )
322
+ ) {
323
+ issues.push(
324
+ PcbAssemblyGltfValidator.#issue(
325
+ 'gltf_accessor_buffer_view_invalid',
326
+ 'Accessor ' +
327
+ index +
328
+ ' references a missing buffer view.'
329
+ )
330
+ )
331
+ }
332
+ if (!(Number(accessor?.count || 0) > 0)) {
333
+ issues.push(
334
+ PcbAssemblyGltfValidator.#issue(
335
+ 'gltf_accessor_count_invalid',
336
+ 'Accessor ' + index + ' must contain values.'
337
+ )
338
+ )
339
+ }
340
+ }
341
+ )
342
+ }
343
+
344
+ /**
345
+ * Validates texture, sampler, and image references.
346
+ * @param {object} gltf GLTF JSON.
347
+ * @param {{ severity: 'error', code: string, message: string }[]} issues Mutable issues.
348
+ * @returns {void}
349
+ */
350
+ static #validateTextures(gltf, issues) {
351
+ PcbAssemblyGltfValidator.#array(gltf?.textures).forEach(
352
+ (texture, index) => {
353
+ if (
354
+ !PcbAssemblyGltfValidator.#hasIndex(
355
+ gltf?.images,
356
+ texture?.source
357
+ )
358
+ ) {
359
+ issues.push(
360
+ PcbAssemblyGltfValidator.#issue(
361
+ 'gltf_texture_image_invalid',
362
+ 'Texture ' + index + ' references a missing image.'
363
+ )
364
+ )
365
+ }
366
+ }
367
+ )
368
+
369
+ PcbAssemblyGltfValidator.#array(gltf?.materials).forEach(
370
+ (material, index) => {
371
+ const textureIndex =
372
+ material?.pbrMetallicRoughness?.baseColorTexture?.index
373
+ if (
374
+ Number.isInteger(textureIndex) &&
375
+ !PcbAssemblyGltfValidator.#hasIndex(
376
+ gltf?.textures,
377
+ textureIndex
378
+ )
379
+ ) {
380
+ issues.push(
381
+ PcbAssemblyGltfValidator.#issue(
382
+ 'gltf_material_texture_invalid',
383
+ 'Material ' +
384
+ index +
385
+ ' references a missing texture.'
386
+ )
387
+ )
388
+ }
389
+ }
390
+ )
391
+ }
392
+
393
+ /**
394
+ * Returns embedded buffer URI byte length.
395
+ * @param {string | undefined} uri Buffer URI.
396
+ * @returns {number}
397
+ */
398
+ static #bufferUriLength(uri) {
399
+ const text = String(uri || '')
400
+ const commaIndex = text.indexOf(',')
401
+ if (!text.startsWith('data:') || commaIndex < 0) {
402
+ return 0
403
+ }
404
+
405
+ const payload = text.slice(commaIndex + 1)
406
+ if (text.slice(0, commaIndex).includes(';base64')) {
407
+ return PcbAssemblyGltfValidator.#base64Length(payload)
408
+ }
409
+
410
+ return decodeURIComponent(payload).length
411
+ }
412
+
413
+ /**
414
+ * Computes decoded base64 byte length.
415
+ * @param {string} value Base64 string.
416
+ * @returns {number}
417
+ */
418
+ static #base64Length(value) {
419
+ const normalized = String(value || '').replace(/=+$/u, '')
420
+ return Math.floor((normalized.length * 3) / 4)
421
+ }
422
+
423
+ /**
424
+ * Returns true when an array contains an integer index.
425
+ * @param {unknown} value Candidate array.
426
+ * @param {unknown} index Candidate index.
427
+ * @returns {boolean}
428
+ */
429
+ static #hasIndex(value, index) {
430
+ return (
431
+ Array.isArray(value) &&
432
+ Number.isInteger(index) &&
433
+ index >= 0 &&
434
+ index < value.length
435
+ )
436
+ }
437
+
438
+ /**
439
+ * Creates one validation issue.
440
+ * @param {string} code Issue code.
441
+ * @param {string} message Human-readable message.
442
+ * @returns {{ severity: 'error', code: string, message: string }}
443
+ */
444
+ static #issue(code, message) {
445
+ return {
446
+ severity: 'error',
447
+ code,
448
+ message
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Normalizes a value to an array.
454
+ * @param {unknown} value Candidate value.
455
+ * @returns {any[]}
456
+ */
457
+ static #array(value) {
458
+ return Array.isArray(value) ? value : []
459
+ }
460
+ }