pcb-scene3d-viewer 1.1.1 → 1.1.2

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.
@@ -1,7 +1,9 @@
1
1
  import { PcbModelArchiveExporter } from './PcbModelArchiveExporter.mjs'
2
+ import { PcbScene3dAdjustmentControlBinder } from './PcbScene3dAdjustmentControlBinder.mjs'
2
3
  import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
3
4
  import { PcbScene3dInteractionHints } from './PcbScene3dInteractionHints.mjs'
4
5
  import { PcbScene3dRuntime } from './PcbScene3dRuntime.mjs'
6
+ import { PcbScene3dSelectionInspectorRenderer } from './PcbScene3dSelectionInspectorRenderer.mjs'
5
7
  import { PcbScene3dText } from './PcbScene3dText.mjs'
6
8
 
7
9
  /**
@@ -23,16 +25,25 @@ export class PcbScene3dController {
23
25
  /** @type {HTMLElement | null} */
24
26
  #selectionNode
25
27
 
28
+ /** @type {HTMLElement | null} */
29
+ #adjustmentHostNode
30
+
31
+ /** @type {PcbScene3dAdjustmentControlBinder | null} */
32
+ #adjustmentControls
33
+
26
34
  /** @type {Array<{ node: EventTarget, type: string, listener: (event: any) => void }>} */
27
35
  #listeners
28
36
 
29
37
  /** @type {Map<string, { component: any | null, externalPlacement: any | null }>} */
30
38
  #selectionIndex
31
39
 
40
+ /** @type {Map<string, { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }>} */
41
+ #componentAdjustments
42
+
32
43
  /** @type {string} */
33
44
  #selectedComponentKey
34
45
 
35
- /** @type {{ setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void } | null} */
46
+ /** @type {{ setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, setSelectedDesignator?: (designator: string) => void, setComponentAdjustment?: (designator: string, adjustment: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }) => void, dispose?: () => void } | null} */
36
47
  #runtime
37
48
 
38
49
  /** @type {any | null} */
@@ -53,6 +64,12 @@ export class PcbScene3dController {
53
64
  /** @type {(key: string) => string} */
54
65
  #translate
55
66
 
67
+ /** @type {boolean} */
68
+ #renderAdjustmentControlsInSelection
69
+
70
+ /** @type {boolean} */
71
+ #autoSearchMissingModels
72
+
56
73
  /** @type {string} */
57
74
  #documentId
58
75
 
@@ -65,7 +82,7 @@ export class PcbScene3dController {
65
82
  /**
66
83
  * @param {HTMLElement} viewportNode
67
84
  * @param {any} documentModel
68
- * @param {{ rootNode?: HTMLElement | null, documentId?: string, onComponentSelectionChange?: ((change: { documentId: string, componentKey: string, source?: string }) => void) | null, sessionAssets?: any[], circuitJson?: object[], sceneDescription?: any, buildScene?: (documentModel: any, options: { modelRegistry: any }) => any, createModelRegistry?: (documentModel: any, sessionAssets: any[]) => any, createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void }, scenePrepClient?: { prepareScene?: (documentModel: any, sessionAssets?: any[]) => Promise<any>, dispose?: () => void } | null, exportArchive?: (options: { archiveBaseName?: string, sceneDescription?: any }) => Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: any[], skippedEntries: any[] }>, downloadArchive?: (archiveName: string, archiveBytes: Uint8Array) => Promise<void> | void, setLoadingVisible?: (visible: boolean) => void, translate?: ((key: string) => string) | null }} [options]
85
+ * @param {{ rootNode?: HTMLElement | null, documentId?: string, onComponentSelectionChange?: ((change: { documentId: string, componentKey: string, source?: string }) => void) | null, sessionAssets?: any[], autoSearchMissingModels?: boolean, circuitJson?: object[], sceneDescription?: any, buildScene?: (documentModel: any, options: { modelRegistry: any }) => any, createModelRegistry?: (documentModel: any, sessionAssets: any[]) => any, createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void }, scenePrepClient?: { prepareScene?: (documentModel: any, sessionAssets?: any[]) => Promise<any>, dispose?: () => void } | null, exportArchive?: (options: { archiveBaseName?: string, sceneDescription?: any }) => Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: any[], skippedEntries: any[] }>, downloadArchive?: (archiveName: string, archiveBytes: Uint8Array) => Promise<void> | void, renderAdjustmentControlsInSelection?: boolean, setLoadingVisible?: (visible: boolean) => void, translate?: ((key: string) => string) | null }} [options]
69
86
  */
70
87
  constructor(viewportNode, documentModel, options = {}) {
71
88
  this.#viewportNode = viewportNode
@@ -81,6 +98,7 @@ export class PcbScene3dController {
81
98
  this.#selectionNode = this.#rootNode?.querySelector(
82
99
  '.scene-3d__selection'
83
100
  )
101
+ this.#adjustmentHostNode = null
84
102
  this.#listeners = []
85
103
  this.#scenePrepClient = options.scenePrepClient || null
86
104
  this.#sceneDescription = null
@@ -99,6 +117,10 @@ export class PcbScene3dController {
99
117
  this.#translate = PcbScene3dText.createTranslator(
100
118
  options.translate || null
101
119
  )
120
+ this.#renderAdjustmentControlsInSelection =
121
+ options.renderAdjustmentControlsInSelection !== false
122
+ this.#autoSearchMissingModels =
123
+ options.autoSearchMissingModels !== false
102
124
  this.#documentId = String(options.documentId || '')
103
125
  this.#onComponentSelectionChange =
104
126
  typeof options.onComponentSelectionChange === 'function'
@@ -107,12 +129,41 @@ export class PcbScene3dController {
107
129
  this.#isDisposed = false
108
130
  this.#runtime = null
109
131
  this.#selectionIndex = new Map()
132
+ this.#componentAdjustments = new Map()
110
133
  this.#selectedComponentKey = ''
134
+ this.#adjustmentControls = new PcbScene3dAdjustmentControlBinder({
135
+ resolveDesignator: () => this.#selectedComponentKey,
136
+ resolveCurrentAdjustment: (designator) =>
137
+ this.#resolveCurrentAdjustment(designator),
138
+ resolveBaselineAdjustment: (designator) =>
139
+ this.#resolveBaselineAdjustment(designator),
140
+ setAdjustment: (designator, adjustment) => {
141
+ this.#componentAdjustments.set(designator, adjustment)
142
+ },
143
+ deleteAdjustment: (designator) => {
144
+ this.#componentAdjustments.delete(designator)
145
+ },
146
+ applyRuntimeAdjustment: (designator, adjustment) => {
147
+ this.#runtime?.setComponentAdjustment?.(designator, adjustment)
148
+ },
149
+ setSelectionHighlightSuppressed: (suppressed) => {
150
+ this.#runtime?.setSelectedDesignator?.(
151
+ suppressed ? '' : this.#selectedComponentKey
152
+ )
153
+ },
154
+ refreshSelection: (designator) => {
155
+ this.#setSelection({
156
+ designator,
157
+ sourceType: this.#resolveSelectionSourceType(designator)
158
+ })
159
+ }
160
+ })
111
161
 
112
162
  this.#bindPresets()
113
163
  this.#setActivePresetButton('isometric')
114
164
  this.#bindToggles()
115
165
  this.#bindExportAction()
166
+ this.#adjustmentControls.bindSelectionNode(this.#selectionNode)
116
167
  this.#setSelection(null)
117
168
  this.#setLoadingVisible(true)
118
169
  const circuitJsonModel = PcbScene3dController.#resolveCircuitJsonModel(
@@ -182,6 +233,46 @@ export class PcbScene3dController {
182
233
  this.#applySelectedComponent()
183
234
  }
184
235
 
236
+ /**
237
+ * Updates app-discovered model visibility without remounting the scene.
238
+ * @param {boolean} enabled Whether app-discovered models should be shown.
239
+ * @returns {void}
240
+ */
241
+ setAutoSearchMissingModels(enabled) {
242
+ this.#autoSearchMissingModels = enabled === true
243
+ this.#runtime?.setToggle?.(
244
+ 'model-search-models',
245
+ this.#autoSearchMissingModels
246
+ )
247
+ }
248
+
249
+ /**
250
+ * Sets an external host for selected-component transform controls.
251
+ * @param {HTMLElement | null} hostNode Sidebar or app-owned controls host.
252
+ * @returns {void}
253
+ */
254
+ setAdjustmentHost(hostNode) {
255
+ const nextHost =
256
+ hostNode && typeof hostNode === 'object' ? hostNode : null
257
+ if (this.#adjustmentHostNode === nextHost) {
258
+ this.#renderAdjustmentHost()
259
+ return
260
+ }
261
+
262
+ this.#adjustmentHostNode = nextHost
263
+ this.#adjustmentControls?.setHost(this.#adjustmentHostNode)
264
+ this.#setSelection(
265
+ this.#selectedComponentKey
266
+ ? {
267
+ designator: this.#selectedComponentKey,
268
+ sourceType: this.#resolveSelectionSourceType(
269
+ this.#selectedComponentKey
270
+ )
271
+ }
272
+ : null
273
+ )
274
+ }
275
+
185
276
  /**
186
277
  * Releases event listeners and runtime resources.
187
278
  * @returns {void}
@@ -194,6 +285,8 @@ export class PcbScene3dController {
194
285
  this.#listeners = []
195
286
  this.#scenePrepClient?.dispose?.()
196
287
  this.#scenePrepClient = null
288
+ this.#adjustmentControls?.dispose()
289
+ this.#adjustmentControls = null
197
290
  this.#runtime?.dispose?.()
198
291
  this.#runtime = null
199
292
  this.#sceneDescription = null
@@ -202,6 +295,7 @@ export class PcbScene3dController {
202
295
  this.#rootNode = null
203
296
  this.#diagnosticsNode = null
204
297
  this.#selectionNode = null
298
+ this.#adjustmentHostNode = null
205
299
  this.#selectedComponentKey = ''
206
300
  this.#exportArchive = async () => ({
207
301
  archiveName: '',
@@ -211,6 +305,7 @@ export class PcbScene3dController {
211
305
  })
212
306
  this.#downloadArchive = async () => {}
213
307
  this.#selectionIndex = new Map()
308
+ this.#componentAdjustments = new Map()
214
309
  }
215
310
 
216
311
  /**
@@ -350,6 +445,9 @@ export class PcbScene3dController {
350
445
  this.#handleRuntimeSelection(selection),
351
446
  translate: this.#translate
352
447
  })
448
+ if (!this.#autoSearchMissingModels) {
449
+ this.#runtime?.setToggle?.('model-search-models', false)
450
+ }
353
451
  if (this.#selectedComponentKey) {
354
452
  this.#applySelectedComponent()
355
453
  }
@@ -608,153 +706,133 @@ export class PcbScene3dController {
608
706
  * @returns {void}
609
707
  */
610
708
  #setSelection(selection) {
611
- if (!this.#selectionNode) {
612
- return
613
- }
614
-
615
709
  const designator = String(selection?.designator || '').trim()
616
710
  if (!designator) {
617
- this.#selectionNode.innerHTML =
618
- '<h4 class="scene-3d__selection-title">' +
619
- PcbScene3dController.#escapeHtml(
620
- this.#translate('scene3d.componentInspector')
621
- ) +
622
- '</h4><p class="scene-3d__selection-empty">' +
623
- PcbScene3dController.#escapeHtml(
624
- this.#translate('scene3d.inspectPrompt')
625
- ) +
626
- '</p>'
711
+ if (this.#selectionNode) {
712
+ this.#selectionNode.innerHTML =
713
+ PcbScene3dSelectionInspectorRenderer.renderEmpty(
714
+ this.#translate
715
+ )
716
+ }
717
+ this.#renderAdjustmentHost(null)
627
718
  return
628
719
  }
629
720
 
630
721
  const selectionEntry = this.#selectionIndex.get(designator)
631
722
  if (!selectionEntry) {
723
+ if (this.#selectionNode) {
724
+ this.#selectionNode.innerHTML =
725
+ PcbScene3dSelectionInspectorRenderer.renderMissing(
726
+ designator,
727
+ this.#translate
728
+ )
729
+ }
730
+ this.#renderAdjustmentHost({
731
+ designator,
732
+ selectionEntry: null
733
+ })
734
+ return
735
+ }
736
+
737
+ const adjustment = this.#resolveCurrentAdjustment(designator)
738
+ if (this.#selectionNode) {
632
739
  this.#selectionNode.innerHTML =
633
- '<h4 class="scene-3d__selection-title">' +
634
- PcbScene3dController.#escapeHtml(
635
- this.#translate('scene3d.componentInspector')
636
- ) +
637
- '</h4><p class="scene-3d__selection-empty">' +
638
- PcbScene3dController.#escapeHtml(
639
- this.#translate('scene3d.noMetadataFor')
640
- ) +
641
- ' ' +
642
- PcbScene3dController.#escapeHtml(designator) +
643
- '.</p>'
740
+ PcbScene3dSelectionInspectorRenderer.renderSelected({
741
+ designator,
742
+ selection,
743
+ selectionEntry,
744
+ adjustment,
745
+ includeControls:
746
+ this.#renderAdjustmentControlsInSelection &&
747
+ !this.#adjustmentHostNode,
748
+ translate: this.#translate
749
+ })
750
+ }
751
+ this.#renderAdjustmentHost({
752
+ designator,
753
+ selection,
754
+ selectionEntry,
755
+ adjustment
756
+ })
757
+ }
758
+
759
+ /**
760
+ * Renders selected component transform controls into the external host.
761
+ * @param {{ designator?: string, selection?: { sourceType?: string } | null, selectionEntry?: { component: any | null, externalPlacement: any | null } | null, adjustment?: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } } } | null} state Selection state.
762
+ * @returns {void}
763
+ */
764
+ #renderAdjustmentHost(state = null) {
765
+ if (!this.#adjustmentHostNode) {
644
766
  return
645
767
  }
646
768
 
647
- const component = selectionEntry.component
648
- const externalPlacement = selectionEntry.externalPlacement
649
- const fields = [
650
- [this.#translate('scene3d.designator'), designator],
651
- [
652
- this.#translate('scene3d.picked'),
653
- selection?.sourceType === 'external-model'
654
- ? this.#translate('scene3d.externalModel')
655
- : this.#translate('scene3d.fallbackBody')
656
- ],
657
- [
658
- this.#translate('scene3d.mountSide'),
659
- externalPlacement?.mountSide || component?.mountSide || ''
660
- ],
661
- [
662
- this.#translate('scene3d.rotation'),
663
- PcbScene3dController.#formatMilValue(
664
- component?.rotationDeg ?? externalPlacement?.rotationDeg,
665
- 'deg'
666
- )
667
- ],
668
- [
669
- this.#translate('scene3d.boardPosition'),
670
- component?.boardPositionMil
671
- ? PcbScene3dController.#formatPoint(
672
- component.boardPositionMil,
673
- true
674
- )
675
- : ''
676
- ],
677
- [
678
- this.#translate('scene3d.pattern'),
679
- String(component?.pattern || '')
680
- ],
681
- [
682
- this.#translate('scene3d.source'),
683
- String(component?.source || '')
684
- ],
685
- [
686
- this.#translate('scene3d.model'),
687
- externalPlacement?.externalModel
688
- ? String(externalPlacement.externalModel.name || '') +
689
- ' (' +
690
- String(externalPlacement.externalModel.format || '') +
691
- ')'
692
- : component?.externalModel
693
- ? String(component.externalModel.name || '') +
694
- ' (' +
695
- String(component.externalModel.format || '') +
696
- ')'
697
- : ''
698
- ],
699
- [
700
- this.#translate('scene3d.bodyPosition'),
701
- externalPlacement?.bodyPositionMil
702
- ? PcbScene3dController.#formatPoint(
703
- externalPlacement.bodyPositionMil,
704
- false
705
- )
706
- : ''
707
- ],
708
- [
709
- this.#translate('scene3d.bodyRotation'),
710
- PcbScene3dController.#formatMilValue(
711
- externalPlacement?.bodyRotationDeg,
712
- 'deg'
713
- )
714
- ],
715
- [
716
- this.#translate('scene3d.modelRotation'),
717
- externalPlacement?.modelTransform?.rotationDeg
718
- ? 'X ' +
719
- PcbScene3dController.#formatNumber(
720
- externalPlacement.modelTransform.rotationDeg.x
721
- ) +
722
- ', Y ' +
723
- PcbScene3dController.#formatNumber(
724
- externalPlacement.modelTransform.rotationDeg.y
725
- ) +
726
- ', Z ' +
727
- PcbScene3dController.#formatNumber(
728
- externalPlacement.modelTransform.rotationDeg.z
729
- )
730
- : ''
731
- ],
732
- [
733
- 'dz',
734
- PcbScene3dController.#formatMilValue(
735
- externalPlacement?.modelTransform?.dzMil,
736
- 'mil'
769
+ const designator = String(
770
+ state?.designator || this.#selectedComponentKey || ''
771
+ ).trim()
772
+ if (!designator) {
773
+ this.#adjustmentHostNode.innerHTML =
774
+ PcbScene3dSelectionInspectorRenderer.renderControlsEmpty(
775
+ this.#translate
737
776
  )
738
- ]
739
- ].filter(([, value]) => String(value || '').trim())
740
-
741
- this.#selectionNode.innerHTML =
742
- '<h4 class="scene-3d__selection-title">' +
743
- PcbScene3dController.#escapeHtml(
744
- this.#translate('scene3d.componentInspector')
745
- ) +
746
- '</h4><dl class="scene-3d__selection-list">' +
747
- fields
748
- .map(
749
- ([label, value]) =>
750
- '<div class="scene-3d__selection-field"><dt>' +
751
- PcbScene3dController.#escapeHtml(label) +
752
- '</dt><dd>' +
753
- PcbScene3dController.#escapeHtml(String(value)) +
754
- '</dd></div>'
777
+ return
778
+ }
779
+
780
+ const selectionEntry =
781
+ state?.selectionEntry === undefined
782
+ ? this.#selectionIndex.get(designator)
783
+ : state.selectionEntry
784
+ if (!selectionEntry) {
785
+ this.#adjustmentHostNode.innerHTML =
786
+ PcbScene3dSelectionInspectorRenderer.renderControlsMissing(
787
+ designator,
788
+ this.#translate
755
789
  )
756
- .join('') +
757
- '</dl>'
790
+ return
791
+ }
792
+
793
+ this.#adjustmentHostNode.innerHTML =
794
+ PcbScene3dSelectionInspectorRenderer.renderControlsPanel({
795
+ designator,
796
+ adjustment:
797
+ state?.adjustment ||
798
+ this.#resolveCurrentAdjustment(designator),
799
+ translate: this.#translate
800
+ })
801
+ }
802
+
803
+ /**
804
+ * Resolves the current in-memory adjustment for one component.
805
+ * @param {string} designator Component designator.
806
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
807
+ */
808
+ #resolveCurrentAdjustment(designator) {
809
+ return (
810
+ this.#componentAdjustments.get(designator) ||
811
+ this.#resolveBaselineAdjustment(designator)
812
+ )
813
+ }
814
+
815
+ /**
816
+ * Resolves the original transform shown before any live edits.
817
+ * @param {string} designator Component designator.
818
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
819
+ */
820
+ #resolveBaselineAdjustment(designator) {
821
+ return PcbScene3dSelectionInspectorRenderer.resolveBaseline(
822
+ this.#selectionIndex,
823
+ designator
824
+ )
825
+ }
826
+
827
+ /**
828
+ * Resolves the current source type label for one selected component.
829
+ * @param {string} designator Component designator.
830
+ * @returns {string}
831
+ */
832
+ #resolveSelectionSourceType(designator) {
833
+ return this.#selectionIndex.get(designator)?.externalPlacement
834
+ ? 'external-model'
835
+ : 'component'
758
836
  }
759
837
 
760
838
  /**
@@ -837,67 +915,6 @@ export class PcbScene3dController {
837
915
  return index
838
916
  }
839
917
 
840
- /**
841
- * Formats one point for the inspector.
842
- * @param {{ x?: number, y?: number, z?: number }} point
843
- * @param {boolean} includeZ
844
- * @returns {string}
845
- */
846
- static #formatPoint(point, includeZ) {
847
- const values = [
848
- 'X ' + PcbScene3dController.#formatNumber(point?.x),
849
- 'Y ' + PcbScene3dController.#formatNumber(point?.y)
850
- ]
851
-
852
- if (includeZ) {
853
- values.push('Z ' + PcbScene3dController.#formatNumber(point?.z))
854
- }
855
-
856
- return values.join(', ') + ' mil'
857
- }
858
-
859
- /**
860
- * Formats one numeric inspector value with an optional unit.
861
- * @param {number | undefined} value
862
- * @param {string} unit
863
- * @returns {string}
864
- */
865
- static #formatMilValue(value, unit) {
866
- if (!Number.isFinite(Number(value))) {
867
- return ''
868
- }
869
-
870
- return PcbScene3dController.#formatNumber(value) + ' ' + unit
871
- }
872
-
873
- /**
874
- * Formats one number for compact UI display.
875
- * @param {number | undefined} value
876
- * @returns {string}
877
- */
878
- static #formatNumber(value) {
879
- const numericValue = Number(value)
880
- if (!Number.isFinite(numericValue)) {
881
- return ''
882
- }
883
-
884
- return numericValue.toFixed(2).replace(/\.00$/, '')
885
- }
886
-
887
- /**
888
- * Escapes user-facing HTML values.
889
- * @param {string} value
890
- * @returns {string}
891
- */
892
- static #escapeHtml(value) {
893
- return String(value)
894
- .replace(/&/g, '&amp;')
895
- .replace(/</g, '&lt;')
896
- .replace(/>/g, '&gt;')
897
- .replace(/"/g, '&quot;')
898
- .replace(/'/g, '&#39;')
899
- }
900
-
901
918
  /**
902
919
  * Resolves one archive base name from the mounted document metadata.
903
920
  * @param {{ summary?: { title?: string }, fileName?: string } | null} documentModel
@@ -263,7 +263,10 @@ export class PcbScene3dCopperFactory {
263
263
  color: 0xd9a61d,
264
264
  roughness: 0.38,
265
265
  metalness: 0.55,
266
- side: THREE.DoubleSide
266
+ side: THREE.DoubleSide,
267
+ polygonOffset: true,
268
+ polygonOffsetFactor: -2,
269
+ polygonOffsetUnits: -2
267
270
  })
268
271
  }
269
272
 
@@ -1,5 +1,6 @@
1
1
  import { PcbScene3dCircularCutoutOverlap } from './PcbScene3dCircularCutoutOverlap.mjs'
2
2
  import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
3
+ import { PcbScene3dGeometryBoundsResolver } from './PcbScene3dGeometryBoundsResolver.mjs'
3
4
 
4
5
  /**
5
6
  * Clips filled 2D geometry against drill-cutout polygons.
@@ -29,7 +30,6 @@ export class PcbScene3dCutoutGeometryFilter {
29
30
  ) {
30
31
  return geometry
31
32
  }
32
-
33
33
  const sourceGeometry =
34
34
  geometry.index && geometry.toNonIndexed
35
35
  ? geometry.toNonIndexed()
@@ -38,9 +38,16 @@ export class PcbScene3dCutoutGeometryFilter {
38
38
  if (!position?.count) {
39
39
  return geometry
40
40
  }
41
-
42
41
  const preparedCutouts =
43
42
  PcbScene3dCutoutGeometryFilter.#prepareCutouts(cutouts)
43
+ if (
44
+ PcbScene3dGeometryBoundsResolver.missesAllPositionBounds(
45
+ position,
46
+ preparedCutouts,
47
+ PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
48
+ )
49
+ )
50
+ return geometry
44
51
  const cutoutIndex =
45
52
  PcbScene3dCutoutGeometryFilter.#buildCutoutSpatialIndex(
46
53
  preparedCutouts
@@ -69,7 +76,6 @@ export class PcbScene3dCutoutGeometryFilter {
69
76
  if (!state.changed) {
70
77
  return geometry
71
78
  }
72
-
73
79
  const filteredGeometry = new THREE.BufferGeometry()
74
80
  filteredGeometry.setAttribute(
75
81
  'position',