pcb-scene3d-viewer 1.1.44 → 1.1.48

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 (41) hide show
  1. package/AGENTS.md +15 -0
  2. package/package.json +1 -1
  3. package/src/PcbAssemblyComponentMeshBuilder.mjs +6 -3
  4. package/src/PcbAssemblyGeometryBuilder.mjs +13 -7
  5. package/src/PcbAssemblyGltfImageDataUri.mjs +67 -0
  6. package/src/PcbAssemblyGltfSceneCamera.mjs +450 -0
  7. package/src/PcbAssemblyGltfWriter.mjs +34 -33
  8. package/src/PcbScene3dBoardAssemblyPresentation.mjs +10 -4
  9. package/src/PcbScene3dBoardCutoutPathFactory.mjs +83 -0
  10. package/src/PcbScene3dBoardMaterialPalette.mjs +17 -2
  11. package/src/PcbScene3dBoardShapeFactory.mjs +16 -3
  12. package/src/PcbScene3dBoardSolderMaskFactory.mjs +20 -10
  13. package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +8 -0
  14. package/src/PcbScene3dController.mjs +29 -5
  15. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +47 -4
  16. package/src/PcbScene3dDrillCutoutFilter.mjs +136 -116
  17. package/src/PcbScene3dDrillVoidFactory.mjs +15 -71
  18. package/src/PcbScene3dExternalCompanionFallback.mjs +48 -3
  19. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +5 -4
  20. package/src/PcbScene3dExternalModelDisplayOutlineTarget.mjs +297 -0
  21. package/src/PcbScene3dExternalModelExactOwnerAnchor.mjs +60 -0
  22. package/src/PcbScene3dExternalModelGroupLoader.mjs +288 -0
  23. package/src/PcbScene3dExternalModelOpacity.mjs +6 -0
  24. package/src/PcbScene3dExternalModelOwnerAnchoredConnectorContactRowRepair.mjs +663 -0
  25. package/src/PcbScene3dExternalModelPadAnchoredBodyCandidate.mjs +273 -0
  26. package/src/PcbScene3dExternalModelPadFallbackCenterRepair.mjs +208 -0
  27. package/src/PcbScene3dExternalModelPlacementRepair.mjs +16 -79
  28. package/src/PcbScene3dExternalModelRepeatedBoundsCenterRepair.mjs +866 -0
  29. package/src/PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.mjs +553 -0
  30. package/src/PcbScene3dExternalModels.mjs +35 -274
  31. package/src/PcbScene3dExternalPlacementDefaults.mjs +241 -0
  32. package/src/PcbScene3dGeometryZCompressor.mjs +5 -2
  33. package/src/PcbScene3dMaskCoveredCopperMaterial.mjs +1 -1
  34. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +172 -5
  35. package/src/PcbScene3dModelSeatingPolicy.mjs +400 -23
  36. package/src/PcbScene3dOutlineBuilder.mjs +54 -0
  37. package/src/PcbScene3dRuntime.mjs +9 -7
  38. package/src/PcbScene3dRuntimeBoardMeshes.mjs +10 -3
  39. package/src/PcbScene3dShellRenderer.mjs +38 -4
  40. package/src/PcbScene3dStaticBodyFactory.mjs +8 -0
  41. package/src/PcbScene3dTransparentMountFaceCuller.mjs +255 -0
@@ -1,4 +1,6 @@
1
1
  import { PcbAssemblyExportCoordinateFrame } from './PcbAssemblyExportCoordinateFrame.mjs'
2
+ import { PcbAssemblyGltfImageDataUri } from './PcbAssemblyGltfImageDataUri.mjs'
3
+ import { PcbAssemblyGltfSceneCamera } from './PcbAssemblyGltfSceneCamera.mjs'
2
4
  import { PcbAssemblyPolygonTriangulator } from './PcbAssemblyPolygonTriangulator.mjs'
3
5
 
4
6
  const FLOAT_COMPONENT = 5126
@@ -17,7 +19,7 @@ const BIN_CHUNK_TYPE = 0x004e4942
17
19
  export class PcbAssemblyGltfWriter {
18
20
  /**
19
21
  * Writes a GLTF or GLB assembly document.
20
- * @param {{ name?: string, meshes?: object[], format?: string, binary?: boolean, includeSceneMetadata?: boolean }} assembly Assembly data.
22
+ * @param {{ name?: string, meshes?: object[], format?: string, binary?: boolean, includeSceneMetadata?: boolean, sceneCameraPreset?: string, sceneCameraAspectRatio?: number, sceneCameraFovDegrees?: number }} assembly Assembly data.
21
23
  * @returns {object | Uint8Array}
22
24
  */
23
25
  static write(assembly = {}) {
@@ -32,7 +34,7 @@ export class PcbAssemblyGltfWriter {
32
34
 
33
35
  /**
34
36
  * Builds the GLTF JSON tree and shared binary buffer.
35
- * @param {{ name?: string, meshes?: object[], includeSceneMetadata?: boolean }} assembly Assembly data.
37
+ * @param {{ name?: string, meshes?: object[], includeSceneMetadata?: boolean, sceneCameraPreset?: string, sceneCameraAspectRatio?: number, sceneCameraFovDegrees?: number }} assembly Assembly data.
36
38
  * @param {boolean} binary Whether the document will be embedded in GLB.
37
39
  * @returns {{ gltf: object, buffer: Uint8Array }}
38
40
  */
@@ -46,7 +48,22 @@ export class PcbAssemblyGltfWriter {
46
48
  PcbAssemblyGltfWriter.#appendMesh(state, mesh)
47
49
  })
48
50
  if (assembly?.includeSceneMetadata === true) {
49
- PcbAssemblyGltfWriter.#appendSceneMetadata(state, sourceMeshes)
51
+ PcbAssemblyGltfWriter.#appendSceneMetadata(
52
+ state,
53
+ sourceMeshes,
54
+ assembly
55
+ )
56
+ }
57
+ if (binary) {
58
+ state.images = PcbAssemblyGltfImageDataUri.pack(
59
+ state.images,
60
+ (bytes) =>
61
+ PcbAssemblyGltfWriter.#appendBufferView(
62
+ state,
63
+ bytes,
64
+ undefined
65
+ )
66
+ )
50
67
  }
51
68
 
52
69
  const buffer = PcbAssemblyGltfWriter.#concatBuffer(state.bufferParts)
@@ -136,41 +153,23 @@ export class PcbAssemblyGltfWriter {
136
153
  * Appends default camera and light nodes from scene bounds.
137
154
  * @param {object} state Writer state.
138
155
  * @param {object[]} meshes Source meshes.
156
+ * @param {{ sceneCameraPreset?: string, sceneCameraAspectRatio?: number, sceneCameraFovDegrees?: number }} options Scene metadata options.
139
157
  * @returns {void}
140
158
  */
141
- static #appendSceneMetadata(state, meshes) {
159
+ static #appendSceneMetadata(state, meshes, options) {
142
160
  const bounds = PcbAssemblyGltfWriter.#sceneBounds(meshes)
143
- const center = [
144
- (bounds.min[0] + bounds.max[0]) / 2,
145
- (bounds.min[1] + bounds.max[1]) / 2,
146
- (bounds.min[2] + bounds.max[2]) / 2
147
- ]
148
- const span = Math.max(
149
- bounds.max[0] - bounds.min[0],
150
- bounds.max[1] - bounds.min[1],
151
- bounds.max[2] - bounds.min[2],
152
- 1
153
- )
154
- const distance = span * 2.2
161
+ const camera = PcbAssemblyGltfSceneCamera.resolve(bounds, options)
155
162
  const cameraIndex = state.cameras.length
156
163
  state.cameras.push({
157
164
  name: 'Default Camera',
158
165
  type: 'perspective',
159
- perspective: {
160
- yfov: 0.7,
161
- znear: Math.max(distance / 1000, 0.01),
162
- zfar: distance * 10
163
- }
166
+ perspective: camera.perspective
164
167
  })
165
168
  state.nodes.push({
166
169
  name: 'Default Camera',
167
170
  camera: cameraIndex,
168
- translation: [
169
- center[0] + distance * 0.65,
170
- center[1] + distance * 0.45,
171
- center[2] + distance
172
- ],
173
- rotation: [-0.260009, 0.279848, 0.076342, 0.920364]
171
+ translation: camera.position,
172
+ rotation: camera.rotation
174
173
  })
175
174
 
176
175
  const lightIndex = state.lights.length
@@ -182,9 +181,9 @@ export class PcbAssemblyGltfWriter {
182
181
  state.nodes.push({
183
182
  name: 'Key Light',
184
183
  translation: [
185
- center[0] - distance * 0.35,
186
- center[1] + distance,
187
- center[2] + distance
184
+ camera.center[0] - camera.distance * 0.35,
185
+ camera.center[1] + camera.distance,
186
+ camera.center[2] + camera.distance
188
187
  ],
189
188
  extensions: {
190
189
  KHR_lights_punctual: {
@@ -513,7 +512,7 @@ export class PcbAssemblyGltfWriter {
513
512
  * Adds one aligned buffer view to the document.
514
513
  * @param {object} state Writer state.
515
514
  * @param {Uint8Array} bytes Binary data.
516
- * @param {number} target Buffer target.
515
+ * @param {number | undefined} target Buffer target.
517
516
  * @returns {number}
518
517
  */
519
518
  static #appendBufferView(state, bytes, target) {
@@ -526,8 +525,10 @@ export class PcbAssemblyGltfWriter {
526
525
  const bufferView = {
527
526
  buffer: 0,
528
527
  byteOffset: state.byteLength,
529
- byteLength: bytes.byteLength,
530
- target
528
+ byteLength: bytes.byteLength
529
+ }
530
+ if (Number.isInteger(target)) {
531
+ bufferView.target = target
531
532
  }
532
533
  state.bufferViews.push(bufferView)
533
534
  state.bufferParts.push(bytes)
@@ -19,9 +19,10 @@ export class PcbScene3dBoardAssemblyPresentation {
19
19
  * PCB-derived substrate, copper, and silkscreen remain the visual source.
20
20
  * @param {any} modelGroup Loaded board assembly group.
21
21
  * @param {{ widthMil?: number, heightMil?: number, thicknessMil?: number } | null | undefined} board Board dimensions.
22
+ * @param {{ sourceFormat?: string }} [options] Presentation options.
22
23
  * @returns {void}
23
24
  */
24
- static apply(modelGroup, board) {
25
+ static apply(modelGroup, board, options = {}) {
25
26
  const meshRecords =
26
27
  PcbScene3dBoardAssemblyPresentation.#collectMeshRecords(modelGroup)
27
28
  const boardBounds =
@@ -30,7 +31,10 @@ export class PcbScene3dBoardAssemblyPresentation {
30
31
  board
31
32
  )
32
33
  const surfaceColor =
33
- PcbScene3dBoardAssemblyPresentation.#resolveSurfaceColor(board)
34
+ PcbScene3dBoardAssemblyPresentation.#resolveSurfaceColor(
35
+ board,
36
+ options
37
+ )
34
38
  const edgeColor =
35
39
  PcbScene3dBoardAssemblyPresentation.#resolveEdgeColor(board)
36
40
  const importedSurfaceColor =
@@ -412,11 +416,13 @@ export class PcbScene3dBoardAssemblyPresentation {
412
416
  /**
413
417
  * Resolves the app-level substrate color for board assembly meshes.
414
418
  * @param {{ surfaceColor?: number } | null | undefined} board Board dimensions.
419
+ * @param {{ sourceFormat?: string }} options Presentation options.
415
420
  * @returns {number}
416
421
  */
417
- static #resolveSurfaceColor(board) {
422
+ static #resolveSurfaceColor(board, options) {
418
423
  return PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
419
- hasBoardAssemblyModel: true
424
+ hasBoardAssemblyModel: true,
425
+ sourceFormat: options?.sourceFormat
420
426
  })
421
427
  }
422
428
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Builds normalized Three.js paths for explicit board cutouts.
3
+ */
4
+ export class PcbScene3dBoardCutoutPathFactory {
5
+ /**
6
+ * Resolves explicit board cutout paths.
7
+ * @param {any} THREE Three.js namespace.
8
+ * @param {{ cutouts?: any[] }} board Board metadata.
9
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
10
+ * @returns {{ path: any, points: { x: number, y: number }[] }[]}
11
+ */
12
+ static resolve(THREE, board, normalizeBoardPoint) {
13
+ return (Array.isArray(board?.cutouts) ? board.cutouts : [])
14
+ .map((cutout) =>
15
+ PcbScene3dBoardCutoutPathFactory.#buildPath(
16
+ THREE,
17
+ cutout,
18
+ normalizeBoardPoint
19
+ )
20
+ )
21
+ .filter(Boolean)
22
+ }
23
+
24
+ /**
25
+ * Builds one explicit board cutout path.
26
+ * @param {any} THREE Three.js namespace.
27
+ * @param {any} cutout Source cutout.
28
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
29
+ * @returns {{ path: any, points: { x: number, y: number }[] } | null}
30
+ */
31
+ static #buildPath(THREE, cutout, normalizeBoardPoint) {
32
+ const points = PcbScene3dBoardCutoutPathFactory.#sourcePoints(cutout)
33
+ .map((point) =>
34
+ PcbScene3dBoardCutoutPathFactory.#normalizePoint(
35
+ point,
36
+ normalizeBoardPoint
37
+ )
38
+ )
39
+ .filter(Boolean)
40
+
41
+ if (points.length < 3) {
42
+ return null
43
+ }
44
+
45
+ const path = new THREE.Path()
46
+ path.moveTo(points[0].x, points[0].y)
47
+ for (let index = 1; index < points.length; index += 1) {
48
+ path.lineTo(points[index].x, points[index].y)
49
+ }
50
+ path.closePath()
51
+
52
+ return { path, points }
53
+ }
54
+
55
+ /**
56
+ * Resolves one cutout's point list.
57
+ * @param {any} cutout Source cutout.
58
+ * @returns {any[]}
59
+ */
60
+ static #sourcePoints(cutout) {
61
+ if (Array.isArray(cutout?.points)) {
62
+ return cutout.points
63
+ }
64
+
65
+ if (Array.isArray(cutout?.vertices)) {
66
+ return cutout.vertices
67
+ }
68
+
69
+ return Array.isArray(cutout) ? cutout : []
70
+ }
71
+
72
+ /**
73
+ * Normalizes one source point into local board shape coordinates.
74
+ * @param {any} point Source point.
75
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
76
+ * @returns {{ x: number, y: number } | null}
77
+ */
78
+ static #normalizePoint(point, normalizeBoardPoint) {
79
+ const x = Number(point?.x ?? point?.[0])
80
+ const y = Number(point?.y ?? point?.[1])
81
+ return Number.isFinite(x + y) ? normalizeBoardPoint(x, y) : null
82
+ }
83
+ }
@@ -7,11 +7,16 @@ export class PcbScene3dBoardMaterialPalette {
7
7
  /**
8
8
  * Resolves the solder-mask face color for the generated board shell.
9
9
  * @param {{ surfaceColor?: number } | undefined} board Board metadata.
10
- * @param {{ hasBoardAssemblyModel?: boolean }} [options] Scene options.
10
+ * @param {{ hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options] Scene options.
11
11
  * @returns {number}
12
12
  */
13
13
  static resolveSurfaceColor(board, options = {}) {
14
- if (options.hasBoardAssemblyModel) {
14
+ if (
15
+ options.hasBoardAssemblyModel &&
16
+ !PcbScene3dBoardMaterialPalette.#shouldPreserveAuthoredSurfaceColor(
17
+ options
18
+ )
19
+ ) {
15
20
  return PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
16
21
  }
17
22
 
@@ -20,6 +25,16 @@ export class PcbScene3dBoardMaterialPalette {
20
25
  : PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
21
26
  }
22
27
 
28
+ /**
29
+ * Returns true when the source format supplies display-stable board colors.
30
+ * @param {{ sourceFormat?: string }} options Scene options.
31
+ * @returns {boolean}
32
+ */
33
+ static #shouldPreserveAuthoredSurfaceColor(options) {
34
+ const sourceFormat = String(options?.sourceFormat || '').toLowerCase()
35
+ return sourceFormat === 'altium' || sourceFormat.startsWith('altium-')
36
+ }
37
+
23
38
  /**
24
39
  * Resolves whether the generated board face should render.
25
40
  * @param {{ hasBoardAssemblyModel?: boolean }} [options] Scene options.
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dBoardEdgeCutoutBuilder } from './PcbScene3dBoardEdgeCutoutBuilder.mjs'
2
+ import { PcbScene3dBoardCutoutPathFactory } from './PcbScene3dBoardCutoutPathFactory.mjs'
2
3
  import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
3
4
  import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
4
5
 
@@ -36,6 +37,11 @@ export class PcbScene3dBoardShapeFactory {
36
37
  )
37
38
  const contourPoints =
38
39
  PcbScene3dBoardEdgeCutoutBuilder.resolveShapePoints(baseShape)
40
+ const boardCutouts = PcbScene3dBoardCutoutPathFactory.resolve(
41
+ THREE,
42
+ board,
43
+ normalizeBoardPoint
44
+ )
39
45
  const drillCutouts = PcbScene3dBoardShapeFactory.#resolveDrillCutouts(
40
46
  THREE,
41
47
  detail,
@@ -61,12 +67,20 @@ export class PcbScene3dBoardShapeFactory {
61
67
  const finalContourPoints = edgeCutouts.length
62
68
  ? PcbScene3dBoardEdgeCutoutBuilder.resolveShapePoints(shape)
63
69
  : contourPoints
64
-
70
+ for (const cutout of boardCutouts) {
71
+ if (
72
+ PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
73
+ cutout.points,
74
+ finalContourPoints
75
+ )
76
+ ) {
77
+ shape.holes.push(cutout.path)
78
+ }
79
+ }
65
80
  for (const cutout of drillCutouts) {
66
81
  if (edgeCutouts.includes(cutout)) {
67
82
  continue
68
83
  }
69
-
70
84
  if (
71
85
  !cutout.isCircular ||
72
86
  PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
@@ -77,7 +91,6 @@ export class PcbScene3dBoardShapeFactory {
77
91
  shape.holes.push(cutout.path)
78
92
  }
79
93
  }
80
-
81
94
  return shape
82
95
  }
83
96
 
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
2
+ import { PcbScene3dBoardCutoutPathFactory } from './PcbScene3dBoardCutoutPathFactory.mjs'
2
3
  import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
3
4
  import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
4
5
  import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
@@ -51,12 +52,14 @@ export class PcbScene3dBoardSolderMaskFactory {
51
52
  const topMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
52
53
  THREE,
53
54
  board,
54
- THREE.FrontSide
55
+ THREE.FrontSide,
56
+ sceneDescription?.sourceFormat
55
57
  )
56
58
  const bottomMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
57
59
  THREE,
58
60
  board,
59
- THREE.BackSide
61
+ THREE.BackSide,
62
+ sceneDescription?.sourceFormat
60
63
  )
61
64
  const z = Number(board?.thicknessMil || 0) / 2
62
65
 
@@ -148,6 +151,11 @@ export class PcbScene3dBoardSolderMaskFactory {
148
151
  )
149
152
  const contourPoints =
150
153
  PcbScene3dBoardSolderMaskFactory.#resolveShapePoints(shape)
154
+ const boardCutouts = PcbScene3dBoardCutoutPathFactory.resolve(
155
+ THREE,
156
+ board,
157
+ normalizeBoardPoint
158
+ )
151
159
  const drillCutouts =
152
160
  PcbScene3dBoardSolderMaskFactory.#resolveDrillCutouts(
153
161
  THREE,
@@ -155,8 +163,8 @@ export class PcbScene3dBoardSolderMaskFactory {
155
163
  normalizeBoardPoint
156
164
  )
157
165
  const { shapeHoles, clippingCutouts } =
158
- PcbScene3dBoardSolderMaskFactory.#partitionDrillCutouts(
159
- drillCutouts,
166
+ PcbScene3dBoardSolderMaskFactory.#partitionCutouts(
167
+ [...boardCutouts, ...drillCutouts],
160
168
  contourPoints
161
169
  )
162
170
 
@@ -395,16 +403,16 @@ export class PcbScene3dBoardSolderMaskFactory {
395
403
  }
396
404
 
397
405
  /**
398
- * Splits drill cutouts into safe shape holes and fallback clip polygons.
399
- * @param {{ path: any, points: { x: number, y: number }[] }[]} drillCutouts
406
+ * Splits cutouts into safe shape holes and fallback clip polygons.
407
+ * @param {{ path: any, points: { x: number, y: number }[] }[]} cutouts
400
408
  * @param {{ x: number, y: number }[]} contourPoints
401
409
  * @returns {{ shapeHoles: { path: any, points: { x: number, y: number }[] }[], clippingCutouts: { path: any, points: { x: number, y: number }[] }[] }}
402
410
  */
403
- static #partitionDrillCutouts(drillCutouts, contourPoints) {
411
+ static #partitionCutouts(cutouts, contourPoints) {
404
412
  const shapeHoles = []
405
413
  const clippingCutouts = []
406
414
 
407
- for (const cutout of Array.isArray(drillCutouts) ? drillCutouts : []) {
415
+ for (const cutout of Array.isArray(cutouts) ? cutouts : []) {
408
416
  if (
409
417
  PcbScene3dBoardSolderMaskFactory.#isHoleInsideContour(
410
418
  cutout.points,
@@ -596,12 +604,14 @@ export class PcbScene3dBoardSolderMaskFactory {
596
604
  * @param {any} THREE
597
605
  * @param {{ surfaceColor?: number }} board Board metadata.
598
606
  * @param {number} side Rendered material side.
607
+ * @param {string | undefined} sourceFormat Scene source format.
599
608
  * @returns {any}
600
609
  */
601
- static #buildMaterial(THREE, board, side) {
610
+ static #buildMaterial(THREE, board, side, sourceFormat) {
602
611
  return new THREE.MeshStandardMaterial({
603
612
  color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
604
- hasBoardAssemblyModel: true
613
+ hasBoardAssemblyModel: true,
614
+ sourceFormat
605
615
  }),
606
616
  ...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
607
617
  polygonOffset: true,
@@ -74,6 +74,14 @@ export class PcbScene3dCompanionBasePlacementAdjuster {
74
74
  return null
75
75
  }
76
76
 
77
+ if (
78
+ PcbScene3dCompanionBasePlacementAdjuster.#isBottomSide(
79
+ component?.mountSide
80
+ )
81
+ ) {
82
+ return null
83
+ }
84
+
77
85
  return {
78
86
  designator: String(component?.designator || '').trim(),
79
87
  mountSide: String(component?.mountSide || 'top').toLowerCase(),
@@ -1,6 +1,7 @@
1
1
  import { PcbModelArchiveExporter } from './PcbModelArchiveExporter.mjs'
2
2
  import { PcbScene3dAdjustmentControlBinder } from './PcbScene3dAdjustmentControlBinder.mjs'
3
3
  import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
4
+ import { PcbScene3dExternalPlacementDefaults } from './PcbScene3dExternalPlacementDefaults.mjs'
4
5
  import { PcbScene3dInteractionHints } from './PcbScene3dInteractionHints.mjs'
5
6
  import { PcbScene3dRuntime } from './PcbScene3dRuntime.mjs'
6
7
  import { PcbScene3dSelectionInspectorRenderer } from './PcbScene3dSelectionInspectorRenderer.mjs'
@@ -473,6 +474,7 @@ export class PcbScene3dController {
473
474
  this.#handleRuntimeSelection(selection),
474
475
  translate: this.#translate
475
476
  })
477
+ this.#applyInitialToggles()
476
478
  if (!this.#autoSearchMissingModels) {
477
479
  this.#runtime?.setToggle?.('model-search-models', false)
478
480
  }
@@ -561,6 +563,25 @@ export class PcbScene3dController {
561
563
  })
562
564
  }
563
565
 
566
+ /**
567
+ * Synchronizes runtime visibility with the shell's initial checkbox state.
568
+ * @returns {void}
569
+ */
570
+ #applyInitialToggles() {
571
+ const toggles =
572
+ this.#rootNode?.querySelectorAll('[data-scene-3d-toggle]') || []
573
+
574
+ toggles.forEach((toggle) => {
575
+ const toggleName =
576
+ toggle?.getAttribute?.('data-scene-3d-toggle') || ''
577
+ if (!toggleName) {
578
+ return
579
+ }
580
+
581
+ this.#runtime?.setToggle?.(toggleName, Boolean(toggle.checked))
582
+ })
583
+ }
584
+
564
585
  /**
565
586
  * Binds the model archive export action.
566
587
  * @returns {void}
@@ -903,11 +924,14 @@ export class PcbScene3dController {
903
924
  * @returns {any}
904
925
  */
905
926
  static #normalizeSceneDescription(sceneDescription) {
906
- return PcbScene3dCircuitJsonAdapter.isDirectCircuitJsonModel(
907
- sceneDescription
908
- )
909
- ? PcbScene3dCircuitJsonAdapter.build(sceneDescription)
910
- : sceneDescription
927
+ const normalizedScene =
928
+ PcbScene3dCircuitJsonAdapter.isDirectCircuitJsonModel(
929
+ sceneDescription
930
+ )
931
+ ? PcbScene3dCircuitJsonAdapter.build(sceneDescription)
932
+ : sceneDescription
933
+
934
+ return PcbScene3dExternalPlacementDefaults.apply(normalizedScene)
911
935
  }
912
936
 
913
937
  /**
@@ -25,9 +25,14 @@ export class PcbScene3dCopperDetailGroupBuilder {
25
25
  */
26
26
  static build(THREE, sceneDescription, topZ, normalizePoint) {
27
27
  const group = new THREE.Group()
28
- const drillCutouts = PcbScene3dCopperDrillCutoutBuilder.resolve(
29
- sceneDescription?.detail
30
- )
28
+ const drillCutouts = [
29
+ ...PcbScene3dCopperDrillCutoutBuilder.resolve(
30
+ sceneDescription?.detail
31
+ ),
32
+ ...PcbScene3dCopperDetailGroupBuilder.#resolveBoardCutouts(
33
+ sceneDescription?.board
34
+ )
35
+ ]
31
36
  const coveredGroup =
32
37
  PcbScene3dCopperDetailGroupBuilder.#buildCoveredGroup(
33
38
  THREE,
@@ -187,6 +192,43 @@ export class PcbScene3dCopperDetailGroupBuilder {
187
192
  )
188
193
  }
189
194
 
195
+ /**
196
+ * Resolves explicit through-board cutouts that copper detail must not cover.
197
+ * @param {{ cutouts?: any[] } | undefined} board Board metadata.
198
+ * @returns {{ x: number, y: number }[][]}
199
+ */
200
+ static #resolveBoardCutouts(board) {
201
+ return (Array.isArray(board?.cutouts) ? board.cutouts : [])
202
+ .map((cutout) =>
203
+ PcbScene3dCopperDetailGroupBuilder.#resolveBoardCutoutPoints(
204
+ cutout
205
+ )
206
+ )
207
+ .filter((points) => points.length >= 3)
208
+ }
209
+
210
+ /**
211
+ * Resolves one explicit board cutout loop in source board coordinates.
212
+ * @param {any} cutout Cutout source.
213
+ * @returns {{ x: number, y: number }[]}
214
+ */
215
+ static #resolveBoardCutoutPoints(cutout) {
216
+ const points = Array.isArray(cutout?.points)
217
+ ? cutout.points
218
+ : Array.isArray(cutout?.vertices)
219
+ ? cutout.vertices
220
+ : Array.isArray(cutout)
221
+ ? cutout
222
+ : []
223
+
224
+ return points
225
+ .map((point) => ({
226
+ x: Number(point?.x ?? point?.[0]),
227
+ y: Number(point?.y ?? point?.[1])
228
+ }))
229
+ .filter((point) => Number.isFinite(point.x + point.y))
230
+ }
231
+
190
232
  /**
191
233
  * Resolves solder-mask material options for covered copper relief.
192
234
  * @param {object} sceneDescription Scene description.
@@ -199,7 +241,8 @@ export class PcbScene3dCopperDetailGroupBuilder {
199
241
  {
200
242
  hasBoardAssemblyModel: Boolean(
201
243
  sceneDescription?.boardAssemblyModel
202
- )
244
+ ),
245
+ sourceFormat: sceneDescription?.sourceFormat
203
246
  }
204
247
  )
205
248
  }