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
@@ -1,6 +1,4 @@
1
- import { PcbScene3dBoardEdgeCutoutBuilder } from './PcbScene3dBoardEdgeCutoutBuilder.mjs'
2
1
  import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
3
- import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
4
2
  import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
5
3
  import { PcbScene3dDrillCutoutFilter } from './PcbScene3dDrillCutoutFilter.mjs'
6
4
  import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
@@ -8,6 +6,7 @@ import { PcbScene3dPolygonOverlap } from './PcbScene3dPolygonOverlap.mjs'
8
6
  import { PcbScene3dShapeHoleMerger } from './PcbScene3dShapeHoleMerger.mjs'
9
7
  import { PcbScene3dShapePathFactory } from './PcbScene3dShapePathFactory.mjs'
10
8
  import { PcbScene3dShapeHoleGeometryCleaner } from './PcbScene3dShapeHoleGeometryCleaner.mjs'
9
+ import { PcbScene3dSilkscreenCutoutContext } from './PcbScene3dSilkscreenCutoutContext.mjs'
11
10
  import { PcbScene3dSilkscreenFillSeamBuilder } from './PcbScene3dSilkscreenFillSeamBuilder.mjs'
12
11
  import { PcbScene3dSilkscreenStrokeWidthResolver } from './PcbScene3dSilkscreenStrokeWidthResolver.mjs'
13
12
  import { PcbScene3dStrokeGeometryBuilder } from './PcbScene3dStrokeGeometryBuilder.mjs'
@@ -19,7 +18,6 @@ import { PcbScene3dTrueTypeTextFactory } from './PcbScene3dTrueTypeTextFactory.m
19
18
  export class PcbScene3dSilkscreenFactory {
20
19
  static #DEFAULT_SILKSCREEN_COLOR = 0xf8f6ef
21
20
  static #FILL_THICKNESS_MIL = 0.8
22
- static #GEOMETRY_EPSILON = 0.001
23
21
  static #FULL_CIRCLE_EPSILON = 0.001
24
22
  static #MIN_STROKE_WIDTH_MIL = 0.04
25
23
  static #STROKE_MESH_POSITION_CHUNK_SIZE = 24000
@@ -102,6 +100,7 @@ export class PcbScene3dSilkscreenFactory {
102
100
  mirrorY
103
101
  )
104
102
  const surfaceCutouts = drillCutouts.concat(copperCutouts)
103
+ const cutoutContext = new PcbScene3dSilkscreenCutoutContext()
105
104
  const strokeMaterial = PcbScene3dSilkscreenFactory.#buildMaterial(
106
105
  THREE,
107
106
  strokeColor
@@ -117,7 +116,8 @@ export class PcbScene3dSilkscreenFactory {
117
116
  normalizeBoardPoint,
118
117
  mirrorY,
119
118
  strokeMaterial,
120
- surfaceCutouts
119
+ surfaceCutouts,
120
+ cutoutContext
121
121
  )
122
122
  const arcMesh = PcbScene3dSilkscreenFactory.#buildArcMesh(
123
123
  THREE,
@@ -126,7 +126,8 @@ export class PcbScene3dSilkscreenFactory {
126
126
  normalizeBoardPoint,
127
127
  mirrorY,
128
128
  strokeMaterial,
129
- surfaceCutouts
129
+ surfaceCutouts,
130
+ cutoutContext
130
131
  )
131
132
  const fillMeshes = PcbScene3dSilkscreenFactory.#buildFillMeshes(
132
133
  THREE,
@@ -135,7 +136,8 @@ export class PcbScene3dSilkscreenFactory {
135
136
  normalizeBoardPoint,
136
137
  mirrorY,
137
138
  fillMaterial,
138
- surfaceCutouts
139
+ surfaceCutouts,
140
+ cutoutContext
139
141
  )
140
142
  const fillSeamMeshes = PcbScene3dSilkscreenFillSeamBuilder.buildMeshes(
141
143
  THREE,
@@ -144,7 +146,10 @@ export class PcbScene3dSilkscreenFactory {
144
146
  normalizeBoardPoint,
145
147
  mirrorY,
146
148
  fillMaterial,
147
- surfaceCutouts
149
+ surfaceCutouts,
150
+ {
151
+ preparedPolygonCache: cutoutContext.preparedPolygonCache
152
+ }
148
153
  )
149
154
  const texts = Array.isArray(silkscreen?.texts) ? silkscreen.texts : []
150
155
  const renderableTexts = texts.filter(
@@ -174,6 +179,7 @@ export class PcbScene3dSilkscreenFactory {
174
179
  fog: false
175
180
  },
176
181
  mirrorY,
182
+ preparedPolygonCache: cutoutContext.preparedPolygonCache,
177
183
  side: mirrorY ? 'bottom' : 'top'
178
184
  }
179
185
  )
@@ -237,6 +243,7 @@ export class PcbScene3dSilkscreenFactory {
237
243
  * @param {boolean} mirrorY
238
244
  * @param {any} material
239
245
  * @param {{ x: number, y: number }[][]} drillCutouts
246
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
240
247
  * @returns {any[]}
241
248
  */
242
249
  static #buildTrackMeshes(
@@ -246,7 +253,8 @@ export class PcbScene3dSilkscreenFactory {
246
253
  normalizeBoardPoint,
247
254
  mirrorY,
248
255
  material,
249
- drillCutouts
256
+ drillCutouts,
257
+ cutoutContext
250
258
  ) {
251
259
  const meshes = []
252
260
  let positions = []
@@ -289,7 +297,8 @@ export class PcbScene3dSilkscreenFactory {
289
297
  THREE,
290
298
  positions,
291
299
  material,
292
- drillCutouts
300
+ drillCutouts,
301
+ cutoutContext
293
302
  )
294
303
  positions = []
295
304
  }
@@ -300,7 +309,8 @@ export class PcbScene3dSilkscreenFactory {
300
309
  THREE,
301
310
  positions,
302
311
  material,
303
- drillCutouts
312
+ drillCutouts,
313
+ cutoutContext
304
314
  )
305
315
 
306
316
  return meshes
@@ -315,6 +325,7 @@ export class PcbScene3dSilkscreenFactory {
315
325
  * @param {boolean} mirrorY
316
326
  * @param {any} material
317
327
  * @param {{ x: number, y: number }[][]} drillCutouts
328
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
318
329
  * @returns {any | null}
319
330
  */
320
331
  static #buildArcMesh(
@@ -324,7 +335,8 @@ export class PcbScene3dSilkscreenFactory {
324
335
  normalizeBoardPoint,
325
336
  mirrorY,
326
337
  material,
327
- drillCutouts
338
+ drillCutouts,
339
+ cutoutContext
328
340
  ) {
329
341
  const positions = []
330
342
 
@@ -353,7 +365,8 @@ export class PcbScene3dSilkscreenFactory {
353
365
  THREE,
354
366
  positions,
355
367
  material,
356
- drillCutouts
368
+ drillCutouts,
369
+ cutoutContext
357
370
  )
358
371
  }
359
372
 
@@ -366,6 +379,7 @@ export class PcbScene3dSilkscreenFactory {
366
379
  * @param {boolean} mirrorY
367
380
  * @param {any} material
368
381
  * @param {{ x: number, y: number }[][]} drillCutouts
382
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
369
383
  * @returns {any[]}
370
384
  */
371
385
  static #buildFillMeshes(
@@ -375,7 +389,8 @@ export class PcbScene3dSilkscreenFactory {
375
389
  normalizeBoardPoint,
376
390
  mirrorY,
377
391
  material,
378
- drillCutouts
392
+ drillCutouts,
393
+ cutoutContext
379
394
  ) {
380
395
  return fills.map((fill) => {
381
396
  const points = PcbScene3dSilkscreenFactory.#normalizeFillPoints(
@@ -402,7 +417,8 @@ export class PcbScene3dSilkscreenFactory {
402
417
  fillHoles,
403
418
  drillCutouts,
404
419
  z,
405
- material
420
+ material,
421
+ cutoutContext
406
422
  )
407
423
  }
408
424
 
@@ -425,6 +441,7 @@ export class PcbScene3dSilkscreenFactory {
425
441
  * @param {{ x: number, y: number }[][]} drillCutouts Normalized drill cutouts.
426
442
  * @param {number} z
427
443
  * @param {any} material
444
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
428
445
  * @returns {any}
429
446
  */
430
447
  static #buildShapeFillMesh(
@@ -433,15 +450,17 @@ export class PcbScene3dSilkscreenFactory {
433
450
  fillHoles,
434
451
  drillCutouts,
435
452
  z,
436
- material
453
+ material,
454
+ cutoutContext
437
455
  ) {
438
456
  const { authoredHoles, drillHoles, uncoveredCutouts } =
439
457
  PcbScene3dDrillCutoutFilter.partitionFillHoles(
440
458
  drillCutouts,
441
- fillHoles
459
+ fillHoles,
460
+ { preparedPolygonCache: cutoutContext.preparedPolygonCache }
442
461
  )
443
462
  const { points: contourPoints, appliedCutouts: edgeCutouts } =
444
- PcbScene3dSilkscreenFactory.#applyCircularEdgeCutouts(
463
+ cutoutContext.applyCircularEdgeCutouts(
445
464
  points,
446
465
  drillHoles.concat(uncoveredCutouts)
447
466
  )
@@ -461,33 +480,41 @@ export class PcbScene3dSilkscreenFactory {
461
480
  clippingHoles: copiedClippingHoles
462
481
  } = PcbScene3dSilkscreenFactory.#partitionDrillCutouts(
463
482
  remainingDrillHoles,
464
- contourPoints
483
+ contourPoints,
484
+ cutoutContext
465
485
  )
466
486
  const { shapeHoles, clippingHoles } =
467
487
  PcbScene3dSilkscreenFactory.#partitionDrillCutouts(
468
488
  remainingUncoveredCutouts,
469
- contourPoints
489
+ contourPoints,
490
+ cutoutContext
470
491
  )
471
492
  const physicalShapeCutoutHoles =
472
493
  PcbScene3dShapeHoleMerger.mergeOverlapping(
473
494
  PcbScene3dDrillCutoutFilter.removeNestedCutouts(
474
- copiedShapeHoles.concat(shapeHoles)
495
+ copiedShapeHoles.concat(shapeHoles),
496
+ {
497
+ preparedPolygonCache: cutoutContext.preparedPolygonCache
498
+ }
475
499
  )
476
500
  )
477
501
  const shapeCutoutHoles = PcbScene3dShapeHoleMerger.mergeOverlapping(
478
502
  PcbScene3dDrillCutoutFilter.removeNestedCutouts(
479
- authoredHoles.concat(copiedShapeHoles, shapeHoles)
503
+ authoredHoles.concat(copiedShapeHoles, shapeHoles),
504
+ { preparedPolygonCache: cutoutContext.preparedPolygonCache }
480
505
  )
481
506
  )
482
507
  const fallbackClippingHoles =
483
508
  PcbScene3dDrillCutoutFilter.removeNestedCutouts(
484
- copiedClippingHoles.concat(clippingHoles)
509
+ copiedClippingHoles.concat(clippingHoles),
510
+ { preparedPolygonCache: cutoutContext.preparedPolygonCache }
485
511
  )
486
512
  PcbScene3dSilkscreenFactory.#appendShapeHoles(
487
513
  THREE,
488
514
  shape,
489
515
  shapeCutoutHoles,
490
- contourPoints
516
+ contourPoints,
517
+ cutoutContext
491
518
  )
492
519
 
493
520
  const shapeFilterHoles =
@@ -496,7 +523,8 @@ export class PcbScene3dSilkscreenFactory {
496
523
  PcbScene3dShapeHoleGeometryCleaner.removeCoveredHoleCenters(
497
524
  THREE,
498
525
  new THREE.ShapeGeometry(shape),
499
- physicalShapeCutoutHoles
526
+ physicalShapeCutoutHoles,
527
+ { preparedPolygonCache: cutoutContext.preparedPolygonCache }
500
528
  )
501
529
  const geometry = PcbScene3dCutoutGeometryFilter.filter(
502
530
  THREE,
@@ -505,7 +533,8 @@ export class PcbScene3dSilkscreenFactory {
505
533
  {
506
534
  maxDepth: 12,
507
535
  maxEdgeLength:
508
- PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH
536
+ PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH,
537
+ preparedPolygonCache: cutoutContext.preparedPolygonCache
509
538
  }
510
539
  )
511
540
  const mesh = new THREE.Mesh(geometry, material)
@@ -513,94 +542,19 @@ export class PcbScene3dSilkscreenFactory {
513
542
  return mesh
514
543
  }
515
544
 
516
- /**
517
- * Converts circular edge cutouts into the fill contour before triangulation.
518
- * @param {{ x: number, y: number }[]} contourPoints Fill contour points.
519
- * @param {{ x: number, y: number }[][]} drillCutouts Candidate cutouts.
520
- * @returns {{ points: { x: number, y: number }[], appliedCutouts: { x: number, y: number }[][] }}
521
- */
522
- static #applyCircularEdgeCutouts(contourPoints, drillCutouts) {
523
- let points = contourPoints
524
- const appliedCutouts = []
525
- const candidates = PcbScene3dDrillCutoutFilter.removeNestedCutouts(
526
- (Array.isArray(drillCutouts) ? drillCutouts : []).filter(
527
- (cutout) =>
528
- PcbScene3dCutoutCircleDetector.resolve(cutout) &&
529
- !PcbScene3dSilkscreenFactory.#isHoleInsideContour(
530
- cutout,
531
- contourPoints
532
- )
533
- )
534
- )
535
-
536
- for (const cutout of candidates) {
537
- const circularCutout =
538
- PcbScene3dCutoutCircleDetector.resolve(cutout)
539
- if (
540
- !circularCutout ||
541
- PcbScene3dSilkscreenFactory.#isHoleInsideContour(cutout, points)
542
- ) {
543
- continue
544
- }
545
-
546
- const nextPoints =
547
- PcbScene3dBoardEdgeCutoutBuilder.applyCircularEdgeCutouts(
548
- points,
549
- [circularCutout]
550
- )
551
- if (
552
- PcbScene3dSilkscreenFactory.#samePointList(points, nextPoints)
553
- ) {
554
- continue
555
- }
556
-
557
- points = nextPoints
558
- appliedCutouts.push(cutout)
559
- }
560
-
561
- return { points, appliedCutouts }
562
- }
563
-
564
- /**
565
- * Returns true when two point lists share identical coordinates.
566
- * @param {{ x: number, y: number }[]} first First point list.
567
- * @param {{ x: number, y: number }[]} second Second point list.
568
- * @returns {boolean}
569
- */
570
- static #samePointList(first, second) {
571
- return (
572
- Array.isArray(first) &&
573
- Array.isArray(second) &&
574
- first.length === second.length &&
575
- first.every((point, index) => {
576
- const otherPoint = second[index]
577
-
578
- return (
579
- Math.abs(point.x - otherPoint.x) <=
580
- PcbScene3dSilkscreenFactory.#GEOMETRY_EPSILON &&
581
- Math.abs(point.y - otherPoint.y) <=
582
- PcbScene3dSilkscreenFactory.#GEOMETRY_EPSILON
583
- )
584
- })
585
- )
586
- }
587
545
  /**
588
546
  * Splits drill cutouts into safe shape holes and fallback clip polygons.
589
547
  * @param {{ x: number, y: number }[][]} drillCutouts
590
548
  * @param {{ x: number, y: number }[]} contourPoints
549
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
591
550
  * @returns {{ shapeHoles: { x: number, y: number }[][], clippingHoles: { x: number, y: number }[][] }}
592
551
  */
593
- static #partitionDrillCutouts(drillCutouts, contourPoints) {
552
+ static #partitionDrillCutouts(drillCutouts, contourPoints, cutoutContext) {
594
553
  const shapeHoles = []
595
554
  const clippingHoles = []
596
555
 
597
556
  for (const cutout of Array.isArray(drillCutouts) ? drillCutouts : []) {
598
- if (
599
- PcbScene3dSilkscreenFactory.#isHoleInsideContour(
600
- cutout,
601
- contourPoints
602
- )
603
- ) {
557
+ if (cutoutContext.isHoleInsideContour(cutout, contourPoints)) {
604
558
  shapeHoles.push(cutout)
605
559
  continue
606
560
  }
@@ -617,20 +571,22 @@ export class PcbScene3dSilkscreenFactory {
617
571
  * @param {{ holes: any[] }} shape
618
572
  * @param {{ x: number, y: number }[][]} holes
619
573
  * @param {{ x: number, y: number }[]} contourPoints
574
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
620
575
  * @returns {void}
621
576
  */
622
- static #appendShapeHoles(THREE, shape, holes, contourPoints) {
577
+ static #appendShapeHoles(
578
+ THREE,
579
+ shape,
580
+ holes,
581
+ contourPoints,
582
+ cutoutContext
583
+ ) {
623
584
  if (!Array.isArray(holes) || !Array.isArray(shape.holes)) {
624
585
  return
625
586
  }
626
587
 
627
588
  for (const points of holes) {
628
- if (
629
- !PcbScene3dSilkscreenFactory.#isHoleInsideContour(
630
- points,
631
- contourPoints
632
- )
633
- ) {
589
+ if (!cutoutContext.isHoleInsideContour(points, contourPoints)) {
634
590
  continue
635
591
  }
636
592
 
@@ -640,19 +596,6 @@ export class PcbScene3dSilkscreenFactory {
640
596
  }
641
597
  }
642
598
 
643
- /**
644
- * Returns true when a cutout can safely be added as a shape hole.
645
- * @param {{ x: number, y: number }[]} hole
646
- * @param {{ x: number, y: number }[]} contour
647
- * @returns {boolean}
648
- */
649
- static #isHoleInsideContour(hole, contour) {
650
- return PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
651
- hole,
652
- contour
653
- )
654
- }
655
-
656
599
  /**
657
600
  * Builds one rectangular fallback fill mesh.
658
601
  * @param {any} THREE
@@ -851,9 +794,16 @@ export class PcbScene3dSilkscreenFactory {
851
794
  * @param {number[]} positions
852
795
  * @param {any} material
853
796
  * @param {{ x: number, y: number }[][]} [drillCutouts]
797
+ * @param {PcbScene3dSilkscreenCutoutContext | null} [cutoutContext]
854
798
  * @returns {any | null}
855
799
  */
856
- static #buildStrokeMesh(THREE, positions, material, drillCutouts = []) {
800
+ static #buildStrokeMesh(
801
+ THREE,
802
+ positions,
803
+ material,
804
+ drillCutouts = [],
805
+ cutoutContext = null
806
+ ) {
857
807
  if (!positions.length) {
858
808
  return null
859
809
  }
@@ -872,7 +822,8 @@ export class PcbScene3dSilkscreenFactory {
872
822
  {
873
823
  maxDepth: 12,
874
824
  maxEdgeLength:
875
- PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH
825
+ PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH,
826
+ preparedPolygonCache: cutoutContext?.preparedPolygonCache
876
827
  }
877
828
  ),
878
829
  material
@@ -886,14 +837,23 @@ export class PcbScene3dSilkscreenFactory {
886
837
  * @param {number[]} positions
887
838
  * @param {any} material
888
839
  * @param {{ x: number, y: number }[][]} drillCutouts
840
+ * @param {PcbScene3dSilkscreenCutoutContext} cutoutContext
889
841
  * @returns {void}
890
842
  */
891
- static #appendStrokeMesh(meshes, THREE, positions, material, drillCutouts) {
843
+ static #appendStrokeMesh(
844
+ meshes,
845
+ THREE,
846
+ positions,
847
+ material,
848
+ drillCutouts,
849
+ cutoutContext
850
+ ) {
892
851
  const mesh = PcbScene3dSilkscreenFactory.#buildStrokeMesh(
893
852
  THREE,
894
853
  positions,
895
854
  material,
896
- drillCutouts
855
+ drillCutouts,
856
+ cutoutContext
897
857
  )
898
858
 
899
859
  if (mesh) {
@@ -22,6 +22,7 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
22
22
  * @param {boolean} mirrorY Whether the side is mirrored.
23
23
  * @param {any} material Shared fill material.
24
24
  * @param {{ x: number, y: number }[][]} [cutouts] Cutouts that should remain uncovered.
25
+ * @param {{ preparedPolygonCache?: Map }} [options] Request-scoped options.
25
26
  * @returns {any[]}
26
27
  */
27
28
  static buildMeshes(
@@ -31,7 +32,8 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
31
32
  normalizeBoardPoint,
32
33
  mirrorY,
33
34
  material,
34
- cutouts = []
35
+ cutouts = [],
36
+ options = {}
35
37
  ) {
36
38
  const meshes = []
37
39
  let positions = []
@@ -55,7 +57,8 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
55
57
  THREE,
56
58
  positions,
57
59
  material,
58
- cutouts
60
+ cutouts,
61
+ options
59
62
  )
60
63
  positions = []
61
64
  }
@@ -66,7 +69,8 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
66
69
  THREE,
67
70
  positions,
68
71
  material,
69
- cutouts
72
+ cutouts,
73
+ options
70
74
  )
71
75
 
72
76
  return meshes
@@ -113,9 +117,10 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
113
117
  * @param {number[]} positions Position buffer.
114
118
  * @param {any} material Shared material.
115
119
  * @param {{ x: number, y: number }[][]} cutouts Cutout polygons.
120
+ * @param {{ preparedPolygonCache?: Map }} options Request-scoped options.
116
121
  * @returns {void}
117
122
  */
118
- static #appendMesh(meshes, THREE, positions, material, cutouts) {
123
+ static #appendMesh(meshes, THREE, positions, material, cutouts, options) {
119
124
  if (
120
125
  !positions.length ||
121
126
  !THREE?.BufferGeometry ||
@@ -133,7 +138,8 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
133
138
  const mesh = new THREE.Mesh(
134
139
  PcbScene3dCutoutGeometryFilter.filter(THREE, geometry, cutouts, {
135
140
  maxDepth: 12,
136
- maxEdgeLength: 2
141
+ maxEdgeLength: 2,
142
+ preparedPolygonCache: options?.preparedPolygonCache
137
143
  }),
138
144
  material
139
145
  )
@@ -1,3 +1,5 @@
1
+ import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
2
+
1
3
  /**
2
4
  * Browser-side STEP mesh loader backed by occt-import-js.
3
5
  */
@@ -76,7 +78,7 @@ export class PcbScene3dStepLoader {
76
78
 
77
79
  /**
78
80
  * Loads one STEP model and normalizes it into triangle payloads.
79
- * @param {{ origin?: string, name?: string, sourceStream?: string, relativePath?: string, payloadText?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
81
+ * @param {{ origin?: string, name?: string, sourceStream?: string, relativePath?: string, payloadText?: string, text?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
80
82
  * @returns {Promise<{ meshPayloads: { name: string, color: number[] | null, positions: ArrayLike<number>, normals: ArrayLike<number>, indices: ArrayLike<number>, faceColors: { first: number, last: number, color: number[] | null }[] }[] }>}
81
83
  */
82
84
  async loadModel(model) {
@@ -99,7 +101,7 @@ export class PcbScene3dStepLoader {
99
101
 
100
102
  /**
101
103
  * Loads one uncached STEP model.
102
- * @param {{ origin?: string, name?: string, sourceStream?: string, relativePath?: string, payloadText?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
104
+ * @param {{ origin?: string, name?: string, sourceStream?: string, relativePath?: string, payloadText?: string, text?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
103
105
  * @returns {Promise<{ meshPayloads: { name: string, color: number[] | null, positions: ArrayLike<number>, normals: ArrayLike<number>, indices: ArrayLike<number>, faceColors: { first: number, last: number, color: number[] | null }[] }[] }>}
104
106
  */
105
107
  async #loadModelUncached(model) {
@@ -290,12 +292,7 @@ export class PcbScene3dStepLoader {
290
292
  * @returns {string}
291
293
  */
292
294
  static #resolveCacheKey(model) {
293
- return [
294
- String(model?.origin || 'unknown'),
295
- String(model?.sourceStream || ''),
296
- String(model?.relativePath || ''),
297
- String(model?.name || '')
298
- ].join('|')
295
+ return PcbScene3dModelIdentity.resolve(model)
299
296
  }
300
297
 
301
298
  /**
@@ -311,12 +308,16 @@ export class PcbScene3dStepLoader {
311
308
 
312
309
  /**
313
310
  * Reads one STEP model into bytes from embedded text, bytes, or a session file.
314
- * @param {{ payloadText?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
311
+ * @param {{ payloadText?: string, text?: string, payloadBytes?: any, bytes?: any, data?: any, file?: Blob | File | Uint8Array | ArrayBuffer | null }} model
315
312
  * @returns {Promise<Uint8Array>}
316
313
  */
317
314
  static async #readModelContent(model) {
318
315
  const payloadText =
319
- typeof model?.payloadText === 'string' ? model.payloadText : ''
316
+ typeof model?.payloadText === 'string'
317
+ ? model.payloadText
318
+ : typeof model?.text === 'string'
319
+ ? model.text
320
+ : ''
320
321
  if (payloadText) {
321
322
  return new TextEncoder().encode(payloadText)
322
323
  }
@@ -34,7 +34,7 @@ const FALLBACK_MESSAGES = {
34
34
  'scene3d.mountSide': 'Mount side',
35
35
  'scene3d.noMetadataFor': 'No metadata is available for',
36
36
  'scene3d.noModelsForExport':
37
- 'No STEP or WRL models were resolved for export.',
37
+ 'No resolved 3D models were available for export.',
38
38
  'scene3d.noPcb': '3D preview is available after loading a PCB document.',
39
39
  'scene3d.pattern': 'Pattern',
40
40
  'scene3d.picked': 'Picked',