brep-io-kernel 1.0.0-ci.10
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.
- package/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- package/src/utils/xformMath.js +51 -0
package/src/UI/viewer.js
ADDED
|
@@ -0,0 +1,4228 @@
|
|
|
1
|
+
// ES6 module
|
|
2
|
+
// Requires three and ArcballControls from three/examples:
|
|
3
|
+
// import * as THREE from 'three';
|
|
4
|
+
// import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls.js';
|
|
5
|
+
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import { ArcballControls } from 'three/examples/jsm/controls/ArcballControls.js';
|
|
8
|
+
import { SVGRenderer } from 'three/examples/jsm/renderers/SVGRenderer.js';
|
|
9
|
+
// Use custom combined translate+rotate gizmo (drop-in for three/examples TransformControls)
|
|
10
|
+
import { CombinedTransformControls } from './controls/CombinedTransformControls.js';
|
|
11
|
+
import { SceneListing } from './SceneListing.js';
|
|
12
|
+
import { CADmaterials, CADmaterialWidget } from './CADmaterials.js';
|
|
13
|
+
import { AccordionWidget } from './AccordionWidget.js';
|
|
14
|
+
import { OrthoCameraIdle } from './OrthoCameraIdle.js';
|
|
15
|
+
import { HistoryWidget } from './HistoryWidget.js';
|
|
16
|
+
import { AssemblyConstraintsWidget } from './assembly/AssemblyConstraintsWidget.js';
|
|
17
|
+
import { PartHistory } from '../PartHistory.js';
|
|
18
|
+
import { SelectionFilter } from './SelectionFilter.js';
|
|
19
|
+
import './expressionsManager.js'
|
|
20
|
+
import { expressionsManager } from './expressionsManager.js';
|
|
21
|
+
import { MainToolbar } from './MainToolbar.js';
|
|
22
|
+
import { registerDefaultToolbarButtons } from './toolbarButtons/registerDefaultButtons.js';
|
|
23
|
+
import { FileManagerWidget } from './fileManagerWidget.js';
|
|
24
|
+
import './mobile.js';
|
|
25
|
+
import { SketchMode3D } from './sketcher/SketchMode3D.js';
|
|
26
|
+
import { ViewCube } from './ViewCube.js';
|
|
27
|
+
import { FloatingWindow } from './FloatingWindow.js';
|
|
28
|
+
import { TriangleDebuggerWindow } from './triangleDebuggerWindow.js';
|
|
29
|
+
import { generateObjectUI } from './objectDump.js';
|
|
30
|
+
import { PluginsWidget } from './PluginsWidget.js';
|
|
31
|
+
import { localStorage as LS } from '../idbStorage.js';
|
|
32
|
+
import { loadSavedPlugins } from '../plugins/pluginManager.js';
|
|
33
|
+
import { PMIViewsWidget } from './pmi/PMIViewsWidget.js';
|
|
34
|
+
import { PMIMode } from './pmi/PMIMode.js';
|
|
35
|
+
import { annotationRegistry } from './pmi/AnnotationRegistry.js';
|
|
36
|
+
import { SchemaForm } from './featureDialogs.js';
|
|
37
|
+
import './dialogs.js';
|
|
38
|
+
import { BREP } from '../BREP/BREP.js';
|
|
39
|
+
import { createAxisHelperGroup, DEFAULT_AXIS_HELPER_PX } from '../utils/axisHelpers.js';
|
|
40
|
+
|
|
41
|
+
const ASSEMBLY_CONSTRAINTS_TITLE = 'Assembly Constraints';
|
|
42
|
+
|
|
43
|
+
function ensureSelectionPickerStyles() {
|
|
44
|
+
if (typeof document === 'undefined') return;
|
|
45
|
+
if (document.getElementById('selection-picker-styles')) return;
|
|
46
|
+
const style = document.createElement('style');
|
|
47
|
+
style.id = 'selection-picker-styles';
|
|
48
|
+
style.textContent = `
|
|
49
|
+
:root {
|
|
50
|
+
--sfw-bg: #121519;
|
|
51
|
+
--sfw-border: #1c2128;
|
|
52
|
+
--sfw-shadow: rgba(0,0,0,0.35);
|
|
53
|
+
--sfw-text: #d6dde6;
|
|
54
|
+
--sfw-accent: #7aa2f7;
|
|
55
|
+
--sfw-muted: #8b98a5;
|
|
56
|
+
--sfw-control-height: 25px;
|
|
57
|
+
}
|
|
58
|
+
.selection-picker {
|
|
59
|
+
position: fixed;
|
|
60
|
+
min-width: 240px;
|
|
61
|
+
max-width: 500px;
|
|
62
|
+
max-height: 260px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
background: linear-gradient(180deg, rgba(18,21,25,0.96), rgba(18,21,25,0.90));
|
|
65
|
+
border: 1px solid var(--sfw-border);
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
box-shadow: 0 12px 30px var(--sfw-shadow);
|
|
68
|
+
color: var(--sfw-text);
|
|
69
|
+
padding: 10px;
|
|
70
|
+
z-index: 1200;
|
|
71
|
+
backdrop-filter: blur(6px);
|
|
72
|
+
opacity: 0.8;
|
|
73
|
+
transition: opacity .15s ease, transform .08s ease;
|
|
74
|
+
}
|
|
75
|
+
.selection-picker.is-hovered,
|
|
76
|
+
.selection-picker.dragging {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
}
|
|
79
|
+
.selection-picker.dragging {
|
|
80
|
+
cursor: grabbing;
|
|
81
|
+
}
|
|
82
|
+
.selection-picker__title {
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
color: var(--sfw-muted);
|
|
85
|
+
letter-spacing: .3px;
|
|
86
|
+
cursor: grab;
|
|
87
|
+
user-select: none;
|
|
88
|
+
border: 1px solid var(--sfw-border);
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
padding: 0 10px;
|
|
91
|
+
background: rgba(255,255,255,0.05);
|
|
92
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
|
93
|
+
flex: 1 1 auto;
|
|
94
|
+
min-height: var(--sfw-control-height);
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
}
|
|
98
|
+
.selection-picker__header {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
margin-bottom: 6px;
|
|
103
|
+
}
|
|
104
|
+
.selection-picker__clear {
|
|
105
|
+
flex: 0 0 auto;
|
|
106
|
+
border-radius: 8px;
|
|
107
|
+
border: 1px solid var(--sfw-border);
|
|
108
|
+
background: rgba(255,255,255,0.08);
|
|
109
|
+
color: var(--sfw-text);
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
padding: 0 12px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
transition: background .12s ease, border-color .12s ease, transform .05s ease;
|
|
114
|
+
min-height: var(--sfw-control-height);
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
}
|
|
118
|
+
.selection-picker__clear:hover {
|
|
119
|
+
background: rgba(122,162,247,0.12);
|
|
120
|
+
border-color: var(--sfw-accent);
|
|
121
|
+
}
|
|
122
|
+
.selection-picker__clear:active {
|
|
123
|
+
transform: translateY(1px);
|
|
124
|
+
}
|
|
125
|
+
.selection-picker__list {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
gap: 6px;
|
|
129
|
+
max-height: 100px;
|
|
130
|
+
overflow: auto;
|
|
131
|
+
padding-top: 3px;
|
|
132
|
+
padding-right: 4px;
|
|
133
|
+
}
|
|
134
|
+
.selection-picker__item {
|
|
135
|
+
width: 100%;
|
|
136
|
+
text-align: left;
|
|
137
|
+
border: 1px solid var(--sfw-border);
|
|
138
|
+
background: rgba(255,255,255,0.04);
|
|
139
|
+
color: var(--sfw-text);
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
padding: 8px 10px;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
transition: border-color .12s ease, transform .08s ease, background .12s ease;
|
|
144
|
+
}
|
|
145
|
+
.selection-picker__item:hover {
|
|
146
|
+
border-color: var(--sfw-accent);
|
|
147
|
+
background: rgba(122,162,247,0.10);
|
|
148
|
+
transform: translateY(-1px);
|
|
149
|
+
}
|
|
150
|
+
.selection-picker__item-label { font-weight: 700; }
|
|
151
|
+
.selection-picker__line {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 8px;
|
|
154
|
+
align-items: center;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
}
|
|
157
|
+
.selection-picker__type {
|
|
158
|
+
font-weight: 700;
|
|
159
|
+
color: var(--sfw-muted);
|
|
160
|
+
flex: 0 0 auto;
|
|
161
|
+
}
|
|
162
|
+
.selection-picker__name {
|
|
163
|
+
flex: 1 1 auto;
|
|
164
|
+
min-width: 0;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
text-overflow: ellipsis;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
document.head.appendChild(style);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ensureSidebarResizerStyles() {
|
|
174
|
+
if (typeof document === 'undefined') return;
|
|
175
|
+
if (document.getElementById('sidebar-resizer-styles')) return;
|
|
176
|
+
const style = document.createElement('style');
|
|
177
|
+
style.id = 'sidebar-resizer-styles';
|
|
178
|
+
style.textContent = `
|
|
179
|
+
#sidebar-resizer {
|
|
180
|
+
position: fixed;
|
|
181
|
+
top: 0;
|
|
182
|
+
width: 10px;
|
|
183
|
+
height: 100%;
|
|
184
|
+
cursor: ew-resize;
|
|
185
|
+
z-index: 8;
|
|
186
|
+
touch-action: none;
|
|
187
|
+
}
|
|
188
|
+
#sidebar-resizer::after {
|
|
189
|
+
content: '';
|
|
190
|
+
position: absolute;
|
|
191
|
+
top: 0;
|
|
192
|
+
left: 50%;
|
|
193
|
+
width: 2px;
|
|
194
|
+
height: 100%;
|
|
195
|
+
transform: translateX(-50%);
|
|
196
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.3), rgba(255,255,255,0.05));
|
|
197
|
+
opacity: 0.5;
|
|
198
|
+
}
|
|
199
|
+
#sidebar-resizer.is-active::after,
|
|
200
|
+
#sidebar-resizer:hover::after {
|
|
201
|
+
opacity: 0.9;
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
document.head.appendChild(style);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function ensureSidebarDockStyles() {
|
|
208
|
+
if (typeof document === 'undefined') return;
|
|
209
|
+
if (document.getElementById('sidebar-dock-styles')) return;
|
|
210
|
+
const style = document.createElement('style');
|
|
211
|
+
style.id = 'sidebar-dock-styles';
|
|
212
|
+
style.textContent = `
|
|
213
|
+
#sidebar-hover-strip {
|
|
214
|
+
position: fixed;
|
|
215
|
+
top: 0;
|
|
216
|
+
left: 0;
|
|
217
|
+
width: 10px;
|
|
218
|
+
height: 100%;
|
|
219
|
+
z-index: 8;
|
|
220
|
+
opacity: 0;
|
|
221
|
+
pointer-events: none;
|
|
222
|
+
background: linear-gradient(90deg, rgba(122,162,247,0.16), rgba(122,162,247,0.00));
|
|
223
|
+
transition: opacity .12s ease;
|
|
224
|
+
}
|
|
225
|
+
#sidebar-hover-strip.is-active {
|
|
226
|
+
opacity: 0.5;
|
|
227
|
+
pointer-events: auto;
|
|
228
|
+
}
|
|
229
|
+
#sidebar-pin-tab {
|
|
230
|
+
position: fixed;
|
|
231
|
+
top: 72px;
|
|
232
|
+
left: 0;
|
|
233
|
+
width: 45px;
|
|
234
|
+
height: 45px;
|
|
235
|
+
border: 1px solid #364053;
|
|
236
|
+
border-left: none;
|
|
237
|
+
border-radius: 0 8px 8px 0;
|
|
238
|
+
background: rgba(20,24,30,.92);
|
|
239
|
+
color: #d6dde6;
|
|
240
|
+
font: 22px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
241
|
+
letter-spacing: 1px;
|
|
242
|
+
text-transform: uppercase;
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
z-index: 9;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
justify-content: center;
|
|
248
|
+
padding: 0;
|
|
249
|
+
user-select: none;
|
|
250
|
+
writing-mode: vertical-rl;
|
|
251
|
+
text-orientation: mixed;
|
|
252
|
+
}
|
|
253
|
+
#sidebar-pin-tab.is-pinned {
|
|
254
|
+
border-color: #6ea8fe;
|
|
255
|
+
color: #e9f0ff;
|
|
256
|
+
box-shadow: 0 0 0 1px rgba(110,168,254,.18) inset;
|
|
257
|
+
}
|
|
258
|
+
#sidebar-pin-tab:active {
|
|
259
|
+
transform: translateY(1px);
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
document.head.appendChild(style);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export class Viewer {
|
|
266
|
+
/**
|
|
267
|
+
* @param {Object} opts
|
|
268
|
+
* @param {HTMLElement} opts.container - DOM node to mount the canvas
|
|
269
|
+
* @param {number} [opts.viewSize=10] - Ortho half-height at zoom=1 (world units)
|
|
270
|
+
* @param {number} [opts.near=-1000]
|
|
271
|
+
* @param {number} [opts.far=1000]
|
|
272
|
+
* @param {number} [opts.pixelRatio=window.devicePixelRatio || 1]
|
|
273
|
+
* @param {THREE.Color | number | string} [opts.clearColor=0x0b0d10] - base clear color (alpha set separately)
|
|
274
|
+
* @param {number} [opts.clearAlpha=0] - clear alpha for transparent captures
|
|
275
|
+
*/
|
|
276
|
+
constructor({
|
|
277
|
+
container,
|
|
278
|
+
viewSize = 10,
|
|
279
|
+
near = -10000000,
|
|
280
|
+
far = 10000000,
|
|
281
|
+
pixelRatio = (window.devicePixelRatio || 1),
|
|
282
|
+
clearColor = 0x0b0d10,
|
|
283
|
+
clearAlpha = 0,
|
|
284
|
+
sidebar = null,
|
|
285
|
+
partHistory = new PartHistory(),
|
|
286
|
+
|
|
287
|
+
}) {
|
|
288
|
+
if (!container) throw new Error('Viewer requires { container }');
|
|
289
|
+
this.BREP = BREP;
|
|
290
|
+
|
|
291
|
+
this.partHistory = partHistory instanceof PartHistory ? partHistory : new PartHistory();
|
|
292
|
+
this._triangleDebugger = null;
|
|
293
|
+
this._lastInspectorTarget = null;
|
|
294
|
+
this._lastInspectorSolid = null;
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
// Core
|
|
300
|
+
this.container = container;
|
|
301
|
+
this.sidebar = sidebar;
|
|
302
|
+
this._sidebarResizer = null;
|
|
303
|
+
this._sidebarResizerCleanup = null;
|
|
304
|
+
this._sidebarPinned = true;
|
|
305
|
+
this._sidebarHoverVisible = false;
|
|
306
|
+
this._sidebarAutoHideSuspended = false;
|
|
307
|
+
this._sidebarPinTab = null;
|
|
308
|
+
this._sidebarHoverStrip = null;
|
|
309
|
+
this._sidebarDockCleanup = null;
|
|
310
|
+
this._sidebarHoverTargets = null;
|
|
311
|
+
this._sidebarStoredDisplay = null;
|
|
312
|
+
this._sidebarStoredVisibility = null;
|
|
313
|
+
this._sidebarStoredTransform = null;
|
|
314
|
+
this._sidebarStoredPointerEvents = null;
|
|
315
|
+
this._sidebarLastPointer = null;
|
|
316
|
+
this._sidebarOffscreen = false;
|
|
317
|
+
this.scene = partHistory instanceof PartHistory ? partHistory.scene : new THREE.Scene();
|
|
318
|
+
this._axisHelpers = new Set();
|
|
319
|
+
this._axisHelpersDirty = true;
|
|
320
|
+
this._axisHelperPx = DEFAULT_AXIS_HELPER_PX;
|
|
321
|
+
try {
|
|
322
|
+
this._worldAxisHelper = createAxisHelperGroup({
|
|
323
|
+
name: "__WORLD_AXES__",
|
|
324
|
+
selectable: false,
|
|
325
|
+
axisHelperPx: this._axisHelperPx,
|
|
326
|
+
});
|
|
327
|
+
this._worldAxisHelper.userData = this._worldAxisHelper.userData || {};
|
|
328
|
+
this._worldAxisHelper.userData.preventRemove = true;
|
|
329
|
+
this.scene.add(this._worldAxisHelper);
|
|
330
|
+
} catch { /* ignore axis helper failures */ }
|
|
331
|
+
ensureSelectionPickerStyles();
|
|
332
|
+
|
|
333
|
+
// Apply persisted sidebar width early (before building UI)
|
|
334
|
+
try {
|
|
335
|
+
if (this.sidebar) {
|
|
336
|
+
const raw = LS.getItem('__CAD_MATERIAL_SETTINGS__');
|
|
337
|
+
if (raw) {
|
|
338
|
+
try {
|
|
339
|
+
const obj = JSON.parse(raw);
|
|
340
|
+
const w = parseInt(obj && obj['__SIDEBAR_WIDTH__']);
|
|
341
|
+
if (Number.isFinite(w) && w > 0) this.sidebar.style.width = `${w}px`;
|
|
342
|
+
} catch { /* ignore parse errors */ }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch { /* ignore */ }
|
|
346
|
+
|
|
347
|
+
this._setupSidebarResizer();
|
|
348
|
+
this._setupSidebarDock();
|
|
349
|
+
|
|
350
|
+
// Renderer
|
|
351
|
+
this.pixelRatio = pixelRatio; // persist for future resizes
|
|
352
|
+
this._clearColor = new THREE.Color(clearColor);
|
|
353
|
+
this._clearAlpha = clearAlpha;
|
|
354
|
+
this._rendererMode = 'webgl';
|
|
355
|
+
this._svgRenderer = null;
|
|
356
|
+
this._webglRenderer = null;
|
|
357
|
+
this.renderer = this._createWebGLRenderer();
|
|
358
|
+
this._webglRenderer = this.renderer;
|
|
359
|
+
this.container.appendChild(this.renderer.domElement);
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
// Camera (Orthographic)
|
|
366
|
+
this.viewSize = viewSize;
|
|
367
|
+
const { width, height } = this._getContainerSize();
|
|
368
|
+
const aspect = width / height || 1;
|
|
369
|
+
this.camera = new OrthoCameraIdle(
|
|
370
|
+
-viewSize * aspect,
|
|
371
|
+
viewSize * aspect,
|
|
372
|
+
viewSize,
|
|
373
|
+
-viewSize,
|
|
374
|
+
near,
|
|
375
|
+
far
|
|
376
|
+
);
|
|
377
|
+
this._defaultNear = near;
|
|
378
|
+
this._defaultFar = far;
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// Camera-anchored light rig: four evenly bright point lights + ambient to keep surfaces lit at any zoom
|
|
384
|
+
const lightIntensity = 5;
|
|
385
|
+
const baseLightRadius = Math.max(15, viewSize * 1.4);
|
|
386
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
|
387
|
+
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x333333, 0.25);
|
|
388
|
+
const lightDirections = [
|
|
389
|
+
[-20, -20, -20],
|
|
390
|
+
[-1, 1, -1],
|
|
391
|
+
[1, -1, -1],
|
|
392
|
+
[-1, -1, 1],
|
|
393
|
+
];
|
|
394
|
+
const pointLights = lightDirections.map(([x, y, z]) => {
|
|
395
|
+
const light = new THREE.PointLight(0xffffff, lightIntensity);
|
|
396
|
+
// No distance attenuation so brightness stays consistent with huge scenes
|
|
397
|
+
light.distance = 0;
|
|
398
|
+
light.decay = 0;
|
|
399
|
+
return light;
|
|
400
|
+
});
|
|
401
|
+
pointLights.forEach((light) => this.camera.add(light));
|
|
402
|
+
this.camera.add(ambientLight);
|
|
403
|
+
this.camera.add(hemiLight);
|
|
404
|
+
this._cameraLightRig = { pointLights, lightDirections, baseLightRadius };
|
|
405
|
+
this._updateCameraLightRig();
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
// Ensure the camera (and its light) participate in the scene graph for lighting calculations
|
|
415
|
+
try { this.camera.userData = { ...(this.camera.userData || {}), preventRemove: true }; } catch { /* ignore */ }
|
|
416
|
+
if (this.camera.parent !== this.scene) {
|
|
417
|
+
try { this.scene.add(this.camera); } catch { /* ignore */ }
|
|
418
|
+
}
|
|
419
|
+
try { this.partHistory.camera = this.camera; } catch { /* ignore */ }
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
// Nice default vantage
|
|
430
|
+
this.camera.position.set(15, 12, 15);
|
|
431
|
+
this.camera.up.set(0, 1, 0);
|
|
432
|
+
this.camera.lookAt(0, 0, 0);
|
|
433
|
+
|
|
434
|
+
// Controls (Arcball)
|
|
435
|
+
this.controls = new ArcballControls(this.camera, this.renderer.domElement, this.scene);
|
|
436
|
+
this.controls.enableAnimations = false;
|
|
437
|
+
this.controls.setGizmosVisible(false);
|
|
438
|
+
this.controls.minDistance = 0.01; // relevant when switching to perspective; harmless here
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
this.camera.enableIdleCallbacks({
|
|
442
|
+
controls: this.controls,
|
|
443
|
+
idleMs: 300,
|
|
444
|
+
onMove: () => {
|
|
445
|
+
// hide sidebar when moving
|
|
446
|
+
if (this.sidebar) {
|
|
447
|
+
this.sidebar.style.opacity = .9;
|
|
448
|
+
}
|
|
449
|
+
this._cameraMoving = true;
|
|
450
|
+
this._updateDepthRange();
|
|
451
|
+
// (quiet) camera moving
|
|
452
|
+
},
|
|
453
|
+
onIdle: () => {
|
|
454
|
+
// show sidebar when idle
|
|
455
|
+
if (this.sidebar) {
|
|
456
|
+
this.sidebar.style.opacity = .9;
|
|
457
|
+
}
|
|
458
|
+
this._cameraMoving = false;
|
|
459
|
+
|
|
460
|
+
// recompute bounding spheres for all geometries (Mesh, Line/Line2, Points)
|
|
461
|
+
this.scene.traverse((object) => {
|
|
462
|
+
const g = object && object.geometry;
|
|
463
|
+
if (g && typeof g.computeBoundingSphere === 'function') {
|
|
464
|
+
try { g.computeBoundingSphere(); } catch (_) { /* noop */ }
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
this._updateDepthRange();
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
// State for interaction
|
|
475
|
+
this._pointerDown = false;
|
|
476
|
+
this._downButton = 0; // 0 left, 2 right
|
|
477
|
+
this._downPos = { x: 0, y: 0 };
|
|
478
|
+
this._dragThreshold = 5; // pixels
|
|
479
|
+
this._raf = null;
|
|
480
|
+
this._disposed = false;
|
|
481
|
+
this._sketchMode = null;
|
|
482
|
+
this._splineMode = null;
|
|
483
|
+
this._imageEditorActive = false;
|
|
484
|
+
this._cameraMoving = false;
|
|
485
|
+
this._sceneBoundsCache = null;
|
|
486
|
+
this._lastPointerEvent = null;
|
|
487
|
+
this._lastDashWpp = null;
|
|
488
|
+
this._selectionOverlay = null;
|
|
489
|
+
this._cubeActive = false;
|
|
490
|
+
// Inspector panel state
|
|
491
|
+
this._inspectorOpen = false;
|
|
492
|
+
this._inspectorEl = null;
|
|
493
|
+
this._inspectorContent = null;
|
|
494
|
+
// Plugin-related state
|
|
495
|
+
this._pendingToolbarButtons = [];
|
|
496
|
+
// Component transform gizmo session state
|
|
497
|
+
this._componentTransformSession = null;
|
|
498
|
+
// Assembly constraints accordion visibility state
|
|
499
|
+
this._assemblyConstraintsVisible = null;
|
|
500
|
+
|
|
501
|
+
// Raycaster for picking
|
|
502
|
+
this.raycaster = new THREE.Raycaster();
|
|
503
|
+
this.raycaster.near = 0;
|
|
504
|
+
this.raycaster.far = Infinity;
|
|
505
|
+
// Initialize params containers; thresholds set per-pick for stability
|
|
506
|
+
try { this.raycaster.params.Line = this.raycaster.params.Line || {}; } catch { }
|
|
507
|
+
try { this.raycaster.params.Line2 = this.raycaster.params.Line2 || {}; } catch { }
|
|
508
|
+
|
|
509
|
+
this._lastCanvasPointerDownAt = 0;
|
|
510
|
+
this._selectionOverlayTimer = null;
|
|
511
|
+
this._pendingSelectionOverlay = null;
|
|
512
|
+
// Bindings
|
|
513
|
+
this._onPointerMove = this._onPointerMove.bind(this);
|
|
514
|
+
this._onPointerDown = this._onPointerDown.bind(this);
|
|
515
|
+
this._onPointerUp = this._onPointerUp.bind(this);
|
|
516
|
+
this._onContextMenu = this._onContextMenu.bind(this);
|
|
517
|
+
this._onResize = this._onResize.bind(this);
|
|
518
|
+
this._onControlsChange = this._onControlsChange.bind(this);
|
|
519
|
+
this._loop = this._loop.bind(this);
|
|
520
|
+
this._updateHover = this._updateHover.bind(this);
|
|
521
|
+
this._selectAt = this._selectAt.bind(this);
|
|
522
|
+
this._onDoubleClick = this._onDoubleClick.bind(this);
|
|
523
|
+
this._onGlobalDoubleClick = this._onGlobalDoubleClick.bind(this);
|
|
524
|
+
this._onPointerLeave = () => {
|
|
525
|
+
try { SelectionFilter.clearHover(); } catch (_) { }
|
|
526
|
+
this._lastPointerEvent = null;
|
|
527
|
+
};
|
|
528
|
+
this._onPointerEnter = (ev) => { this._lastPointerEvent = ev; };
|
|
529
|
+
|
|
530
|
+
// Events
|
|
531
|
+
const el = this.renderer.domElement;
|
|
532
|
+
this._attachRendererEvents(el);
|
|
533
|
+
|
|
534
|
+
SelectionFilter.viewer = this;
|
|
535
|
+
// Use capture on pointerup to ensure we end interactions even if pointerup fires off-element
|
|
536
|
+
window.addEventListener('pointerup', this._onPointerUp, { passive: false, capture: true });
|
|
537
|
+
document.addEventListener('dblclick', this._onGlobalDoubleClick, { passive: false, capture: true });
|
|
538
|
+
window.addEventListener('resize', this._onResize);
|
|
539
|
+
this._onKeyDown = this._onKeyDown.bind(this);
|
|
540
|
+
window.addEventListener('keydown', this._onKeyDown, { passive: false });
|
|
541
|
+
// Keep camera updates; no picking to sync
|
|
542
|
+
this.controls.addEventListener('change', this._onControlsChange);
|
|
543
|
+
|
|
544
|
+
this.SelectionFilter = SelectionFilter;
|
|
545
|
+
|
|
546
|
+
// Expose annotation registry for PMI modules and plugins
|
|
547
|
+
this.annotationRegistry = annotationRegistry;
|
|
548
|
+
|
|
549
|
+
// View cube overlay
|
|
550
|
+
this._ensureViewCube();
|
|
551
|
+
|
|
552
|
+
// Initial sizing + start
|
|
553
|
+
this._resizeRendererToDisplaySize();
|
|
554
|
+
this._loop();
|
|
555
|
+
this.setupAccordion();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_createWebGLRenderer() {
|
|
559
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
|
|
560
|
+
renderer.setClearColor(this._clearColor, this._clearAlpha);
|
|
561
|
+
renderer.setPixelRatio(this.pixelRatio || 1);
|
|
562
|
+
this._applyRendererElementStyles(renderer);
|
|
563
|
+
return renderer;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_createSvgRenderer() {
|
|
567
|
+
const renderer = new SVGRenderer();
|
|
568
|
+
renderer.setQuality('high');
|
|
569
|
+
renderer.setClearColor(this._clearColor);
|
|
570
|
+
this._applyRendererElementStyles(renderer);
|
|
571
|
+
return renderer;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_applyRendererElementStyles(renderer) {
|
|
575
|
+
const el = renderer?.domElement;
|
|
576
|
+
if (!el) return;
|
|
577
|
+
el.style.display = 'block';
|
|
578
|
+
el.style.outline = 'none';
|
|
579
|
+
el.style.userSelect = 'none';
|
|
580
|
+
el.style.width = '100%';
|
|
581
|
+
el.style.height = '100%';
|
|
582
|
+
el.style.background = this._clearAlpha === 0 ? 'transparent' : this._clearColor.getStyle();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_attachRendererEvents(el) {
|
|
586
|
+
if (!el) return;
|
|
587
|
+
el.addEventListener('pointermove', this._onPointerMove, { passive: true });
|
|
588
|
+
el.addEventListener('pointerleave', this._onPointerLeave, { passive: true });
|
|
589
|
+
el.addEventListener('pointerenter', this._onPointerEnter, { passive: true });
|
|
590
|
+
el.addEventListener('pointerdown', this._onPointerDown, { passive: false });
|
|
591
|
+
el.addEventListener('dblclick', this._onDoubleClick, { passive: false });
|
|
592
|
+
el.addEventListener('contextmenu', this._onContextMenu);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
_detachRendererEvents(el) {
|
|
596
|
+
if (!el) return;
|
|
597
|
+
el.removeEventListener('pointermove', this._onPointerMove);
|
|
598
|
+
el.removeEventListener('pointerleave', this._onPointerLeave);
|
|
599
|
+
el.removeEventListener('pointerenter', this._onPointerEnter);
|
|
600
|
+
el.removeEventListener('pointerdown', this._onPointerDown);
|
|
601
|
+
el.removeEventListener('dblclick', this._onDoubleClick);
|
|
602
|
+
el.removeEventListener('contextmenu', this._onContextMenu);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
_rebuildControls(domElement) {
|
|
606
|
+
const prev = this.controls;
|
|
607
|
+
const prevState = prev ? {
|
|
608
|
+
target: prev.target ? prev.target.clone() : null,
|
|
609
|
+
enabled: prev.enabled,
|
|
610
|
+
minDistance: prev.minDistance,
|
|
611
|
+
maxDistance: prev.maxDistance,
|
|
612
|
+
enableAnimations: prev.enableAnimations
|
|
613
|
+
} : null;
|
|
614
|
+
try { prev?.removeEventListener?.('change', this._onControlsChange); } catch { }
|
|
615
|
+
try { prev?.dispose?.(); } catch { }
|
|
616
|
+
|
|
617
|
+
const controls = new ArcballControls(this.camera, domElement, this.scene);
|
|
618
|
+
controls.enableAnimations = prevState ? !!prevState.enableAnimations : false;
|
|
619
|
+
controls.setGizmosVisible(false);
|
|
620
|
+
controls.minDistance = prevState && Number.isFinite(prevState.minDistance) ? prevState.minDistance : 0.01;
|
|
621
|
+
if (prevState && Number.isFinite(prevState.maxDistance)) controls.maxDistance = prevState.maxDistance;
|
|
622
|
+
if (prevState?.target) controls.target.copy(prevState.target);
|
|
623
|
+
if (typeof prevState?.enabled === 'boolean') controls.enabled = prevState.enabled;
|
|
624
|
+
this.controls = controls;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_ensureViewCube() {
|
|
628
|
+
if (this.viewCube && this.viewCube.renderer === this.renderer) return;
|
|
629
|
+
this.viewCube = new ViewCube({
|
|
630
|
+
renderer: this.renderer,
|
|
631
|
+
targetCamera: this.camera,
|
|
632
|
+
controls: this.controls,
|
|
633
|
+
size: 120,
|
|
634
|
+
margin: 12,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
setRendererMode(mode) {
|
|
639
|
+
const nextMode = mode === 'svg' ? 'svg' : 'webgl';
|
|
640
|
+
if (nextMode === this._rendererMode && this.renderer) return;
|
|
641
|
+
this._rendererMode = nextMode;
|
|
642
|
+
|
|
643
|
+
try { this._stopComponentTransformSession?.(); } catch { }
|
|
644
|
+
|
|
645
|
+
const prevEl = this.renderer?.domElement;
|
|
646
|
+
this._detachRendererEvents(prevEl);
|
|
647
|
+
if (prevEl && prevEl.parentNode) prevEl.parentNode.removeChild(prevEl);
|
|
648
|
+
|
|
649
|
+
let nextRenderer = null;
|
|
650
|
+
if (nextMode === 'svg') {
|
|
651
|
+
if (!this._svgRenderer) this._svgRenderer = this._createSvgRenderer();
|
|
652
|
+
nextRenderer = this._svgRenderer;
|
|
653
|
+
} else {
|
|
654
|
+
if (!this._webglRenderer) this._webglRenderer = this._createWebGLRenderer();
|
|
655
|
+
nextRenderer = this._webglRenderer;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
this.renderer = nextRenderer;
|
|
659
|
+
this._applyRendererElementStyles(this.renderer);
|
|
660
|
+
this.container.appendChild(this.renderer.domElement);
|
|
661
|
+
this._attachRendererEvents(this.renderer.domElement);
|
|
662
|
+
this._rebuildControls(this.renderer.domElement);
|
|
663
|
+
try { this.controls?.addEventListener?.('change', this._onControlsChange); } catch { }
|
|
664
|
+
try { this.camera?.attachControls?.(this.controls); } catch { }
|
|
665
|
+
|
|
666
|
+
if (nextMode === 'webgl') {
|
|
667
|
+
this._ensureViewCube();
|
|
668
|
+
} else {
|
|
669
|
+
this.viewCube = null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
try { this.renderer.domElement.style.marginTop = '0px'; } catch { }
|
|
673
|
+
this._resizeRendererToDisplaySize();
|
|
674
|
+
this.render();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
_setupSidebarResizer() {
|
|
678
|
+
if (!this.sidebar || this._sidebarResizer) return;
|
|
679
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
680
|
+
ensureSidebarResizerStyles();
|
|
681
|
+
try {
|
|
682
|
+
const existing = document.getElementById('sidebar-resizer');
|
|
683
|
+
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
|
684
|
+
} catch { /* ignore */ }
|
|
685
|
+
|
|
686
|
+
const resizer = document.createElement('div');
|
|
687
|
+
resizer.id = 'sidebar-resizer';
|
|
688
|
+
resizer.title = 'Drag to resize sidebar';
|
|
689
|
+
resizer.setAttribute('aria-hidden', 'true');
|
|
690
|
+
document.body.appendChild(resizer);
|
|
691
|
+
this._sidebarResizer = resizer;
|
|
692
|
+
|
|
693
|
+
const handleWidth = 10;
|
|
694
|
+
resizer.style.width = `${handleWidth}px`;
|
|
695
|
+
|
|
696
|
+
const updatePosition = () => {
|
|
697
|
+
if (!this.sidebar) return;
|
|
698
|
+
const rect = this.sidebar.getBoundingClientRect();
|
|
699
|
+
const hidden = !this._isSidebarVisible();
|
|
700
|
+
if (hidden || rect.width <= 0 || rect.height <= 0) {
|
|
701
|
+
resizer.style.display = 'none';
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
resizer.style.display = '';
|
|
705
|
+
resizer.style.left = `${Math.round(rect.right - handleWidth / 2)}px`;
|
|
706
|
+
resizer.style.top = `${Math.round(rect.top)}px`;
|
|
707
|
+
resizer.style.height = `${Math.round(rect.height)}px`;
|
|
708
|
+
try { this._positionSidebarPinTab?.(); } catch { /* ignore */ }
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const clampWidth = (value) => {
|
|
712
|
+
let v = Number(value);
|
|
713
|
+
if (!Number.isFinite(v)) return 200;
|
|
714
|
+
const input = this.cadMaterialsUi?._widthInput;
|
|
715
|
+
const min = Number(input?.min) || 200;
|
|
716
|
+
const max = Number(input?.max) || 600;
|
|
717
|
+
if (v < min) v = min; else if (v > max) v = max;
|
|
718
|
+
return Math.round(v);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const persistWidthFallback = (value) => {
|
|
722
|
+
try {
|
|
723
|
+
const raw = LS.getItem('__CAD_MATERIAL_SETTINGS__');
|
|
724
|
+
const settings = raw ? JSON.parse(raw) : {};
|
|
725
|
+
settings['__SIDEBAR_WIDTH__'] = value;
|
|
726
|
+
LS.setItem('__CAD_MATERIAL_SETTINGS__', JSON.stringify(settings, null, 2));
|
|
727
|
+
} catch { /* ignore */ }
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const applyWidth = (value, { persist = false } = {}) => {
|
|
731
|
+
const next = clampWidth(value);
|
|
732
|
+
if (this.cadMaterialsUi && typeof this.cadMaterialsUi.setSidebarWidth === 'function') {
|
|
733
|
+
this.cadMaterialsUi.setSidebarWidth(next, { persist });
|
|
734
|
+
} else if (this.sidebar) {
|
|
735
|
+
this.sidebar.style.width = `${next}px`;
|
|
736
|
+
if (persist) persistWidthFallback(next);
|
|
737
|
+
}
|
|
738
|
+
updatePosition();
|
|
739
|
+
return next;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const dragState = {
|
|
743
|
+
active: false,
|
|
744
|
+
startX: 0,
|
|
745
|
+
startWidth: 0,
|
|
746
|
+
lastWidth: 0,
|
|
747
|
+
pointerId: null,
|
|
748
|
+
prevCursor: '',
|
|
749
|
+
prevUserSelect: '',
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const startDrag = (ev) => {
|
|
753
|
+
if (ev.button !== 0 || !this.sidebar) return;
|
|
754
|
+
ev.preventDefault();
|
|
755
|
+
dragState.active = true;
|
|
756
|
+
dragState.startX = ev.clientX;
|
|
757
|
+
dragState.startWidth = this.sidebar.getBoundingClientRect().width;
|
|
758
|
+
dragState.lastWidth = dragState.startWidth;
|
|
759
|
+
dragState.pointerId = ev.pointerId;
|
|
760
|
+
dragState.prevCursor = document.body.style.cursor;
|
|
761
|
+
dragState.prevUserSelect = document.body.style.userSelect;
|
|
762
|
+
document.body.style.cursor = 'ew-resize';
|
|
763
|
+
document.body.style.userSelect = 'none';
|
|
764
|
+
resizer.classList.add('is-active');
|
|
765
|
+
try { resizer.setPointerCapture(ev.pointerId); } catch { /* ignore */ }
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const onDragMove = (ev) => {
|
|
769
|
+
if (!dragState.active) return;
|
|
770
|
+
const delta = ev.clientX - dragState.startX;
|
|
771
|
+
dragState.lastWidth = applyWidth(dragState.startWidth + delta);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const stopDrag = (persist = true) => {
|
|
775
|
+
if (!dragState.active) return;
|
|
776
|
+
dragState.active = false;
|
|
777
|
+
resizer.classList.remove('is-active');
|
|
778
|
+
document.body.style.cursor = dragState.prevCursor || '';
|
|
779
|
+
document.body.style.userSelect = dragState.prevUserSelect || '';
|
|
780
|
+
const finalWidth = Number.isFinite(dragState.lastWidth) ? dragState.lastWidth : dragState.startWidth;
|
|
781
|
+
applyWidth(finalWidth, { persist });
|
|
782
|
+
if (dragState.pointerId != null) {
|
|
783
|
+
try { resizer.releasePointerCapture(dragState.pointerId); } catch { /* ignore */ }
|
|
784
|
+
}
|
|
785
|
+
dragState.pointerId = null;
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const onPointerUp = () => stopDrag(true);
|
|
789
|
+
const onPointerCancel = () => stopDrag(false);
|
|
790
|
+
const onWindowPointerUp = () => stopDrag(true);
|
|
791
|
+
const onWindowResize = () => updatePosition();
|
|
792
|
+
|
|
793
|
+
resizer.addEventListener('pointerdown', startDrag);
|
|
794
|
+
resizer.addEventListener('pointermove', onDragMove);
|
|
795
|
+
resizer.addEventListener('pointerup', onPointerUp);
|
|
796
|
+
resizer.addEventListener('pointercancel', onPointerCancel);
|
|
797
|
+
window.addEventListener('pointerup', onWindowPointerUp, { capture: true });
|
|
798
|
+
window.addEventListener('resize', onWindowResize);
|
|
799
|
+
|
|
800
|
+
let ro = null;
|
|
801
|
+
try {
|
|
802
|
+
if (window.ResizeObserver) {
|
|
803
|
+
ro = new ResizeObserver(() => updatePosition());
|
|
804
|
+
ro.observe(this.sidebar);
|
|
805
|
+
}
|
|
806
|
+
} catch { /* ignore */ }
|
|
807
|
+
|
|
808
|
+
let mo = null;
|
|
809
|
+
try {
|
|
810
|
+
if (window.MutationObserver) {
|
|
811
|
+
mo = new MutationObserver(() => updatePosition());
|
|
812
|
+
mo.observe(this.sidebar, { attributes: true, attributeFilter: ['style', 'hidden', 'class'] });
|
|
813
|
+
}
|
|
814
|
+
} catch { /* ignore */ }
|
|
815
|
+
|
|
816
|
+
updatePosition();
|
|
817
|
+
|
|
818
|
+
this._sidebarResizerCleanup = () => {
|
|
819
|
+
try { stopDrag(false); } catch { /* ignore */ }
|
|
820
|
+
resizer.removeEventListener('pointerdown', startDrag);
|
|
821
|
+
resizer.removeEventListener('pointermove', onDragMove);
|
|
822
|
+
resizer.removeEventListener('pointerup', onPointerUp);
|
|
823
|
+
resizer.removeEventListener('pointercancel', onPointerCancel);
|
|
824
|
+
window.removeEventListener('pointerup', onWindowPointerUp, { capture: true });
|
|
825
|
+
window.removeEventListener('resize', onWindowResize);
|
|
826
|
+
try { ro && ro.disconnect(); } catch { /* ignore */ }
|
|
827
|
+
try { mo && mo.disconnect(); } catch { /* ignore */ }
|
|
828
|
+
if (resizer.parentNode) resizer.parentNode.removeChild(resizer);
|
|
829
|
+
if (this._sidebarResizer === resizer) this._sidebarResizer = null;
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
_setupSidebarDock() {
|
|
834
|
+
if (!this.sidebar || this._sidebarPinTab) return;
|
|
835
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
836
|
+
ensureSidebarDockStyles();
|
|
837
|
+
try {
|
|
838
|
+
const existingTab = document.getElementById('sidebar-pin-tab');
|
|
839
|
+
if (existingTab && existingTab.parentNode) existingTab.parentNode.removeChild(existingTab);
|
|
840
|
+
} catch { /* ignore */ }
|
|
841
|
+
try {
|
|
842
|
+
const existingStrip = document.getElementById('sidebar-hover-strip');
|
|
843
|
+
if (existingStrip && existingStrip.parentNode) existingStrip.parentNode.removeChild(existingStrip);
|
|
844
|
+
} catch { /* ignore */ }
|
|
845
|
+
|
|
846
|
+
const hoverStrip = document.createElement('div');
|
|
847
|
+
hoverStrip.id = 'sidebar-hover-strip';
|
|
848
|
+
hoverStrip.setAttribute('aria-hidden', 'true');
|
|
849
|
+
document.body.appendChild(hoverStrip);
|
|
850
|
+
this._sidebarHoverStrip = hoverStrip;
|
|
851
|
+
|
|
852
|
+
const pinTab = document.createElement('button');
|
|
853
|
+
pinTab.id = 'sidebar-pin-tab';
|
|
854
|
+
pinTab.type = 'button';
|
|
855
|
+
pinTab.textContent = '📌';
|
|
856
|
+
pinTab.setAttribute('aria-pressed', 'true');
|
|
857
|
+
pinTab.title = 'Collapse sidebar';
|
|
858
|
+
document.body.appendChild(pinTab);
|
|
859
|
+
this._sidebarPinTab = pinTab;
|
|
860
|
+
|
|
861
|
+
const hoverTargets = new Set();
|
|
862
|
+
this._sidebarHoverTargets = hoverTargets;
|
|
863
|
+
let hoverUpdateRaf = null;
|
|
864
|
+
const scheduleHoverUpdate = () => {
|
|
865
|
+
if (this._sidebarPinned || this._sidebarAutoHideSuspended) return;
|
|
866
|
+
if (hoverUpdateRaf != null) cancelAnimationFrame(hoverUpdateRaf);
|
|
867
|
+
hoverUpdateRaf = requestAnimationFrame(() => {
|
|
868
|
+
hoverUpdateRaf = null;
|
|
869
|
+
if (this._sidebarPinned || this._sidebarAutoHideSuspended) return;
|
|
870
|
+
this._setSidebarHoverVisible(hoverTargets.size > 0);
|
|
871
|
+
});
|
|
872
|
+
};
|
|
873
|
+
const bindHover = (el, { captureSidebarOnLeave = false, capturePinOnLeave = false, requireSidebarVisible = false } = {}) => {
|
|
874
|
+
const onEnter = () => {
|
|
875
|
+
if (requireSidebarVisible && !this._isSidebarVisible()) return;
|
|
876
|
+
hoverTargets.add(el);
|
|
877
|
+
scheduleHoverUpdate();
|
|
878
|
+
};
|
|
879
|
+
const onLeave = (ev) => {
|
|
880
|
+
hoverTargets.delete(el);
|
|
881
|
+
const pinTabEl = this._sidebarPinTab;
|
|
882
|
+
if (capturePinOnLeave && pinTabEl) {
|
|
883
|
+
const related = ev?.relatedTarget;
|
|
884
|
+
if (related === pinTabEl || (pinTabEl.contains && pinTabEl.contains(related))) {
|
|
885
|
+
hoverTargets.add(pinTabEl);
|
|
886
|
+
scheduleHoverUpdate();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (capturePinOnLeave && pinTabEl && this._isSidebarVisible()) {
|
|
891
|
+
const rect = pinTabEl.getBoundingClientRect();
|
|
892
|
+
if (rect
|
|
893
|
+
&& ev
|
|
894
|
+
&& ev.clientX >= rect.left
|
|
895
|
+
&& ev.clientX <= rect.right
|
|
896
|
+
&& ev.clientY >= rect.top
|
|
897
|
+
&& ev.clientY <= rect.bottom) {
|
|
898
|
+
hoverTargets.add(pinTabEl);
|
|
899
|
+
scheduleHoverUpdate();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (captureSidebarOnLeave && this.sidebar && this._isSidebarVisible()) {
|
|
904
|
+
const rect = this.sidebar.getBoundingClientRect();
|
|
905
|
+
if (rect
|
|
906
|
+
&& ev
|
|
907
|
+
&& ev.clientX >= rect.left
|
|
908
|
+
&& ev.clientX <= rect.right
|
|
909
|
+
&& ev.clientY >= rect.top
|
|
910
|
+
&& ev.clientY <= rect.bottom) {
|
|
911
|
+
hoverTargets.add(this.sidebar);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
scheduleHoverUpdate();
|
|
915
|
+
};
|
|
916
|
+
el.addEventListener('pointerenter', onEnter);
|
|
917
|
+
el.addEventListener('pointerleave', onLeave);
|
|
918
|
+
return () => {
|
|
919
|
+
el.removeEventListener('pointerenter', onEnter);
|
|
920
|
+
el.removeEventListener('pointerleave', onLeave);
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const cleanup = [];
|
|
925
|
+
cleanup.push(bindHover(hoverStrip, { captureSidebarOnLeave: true }));
|
|
926
|
+
cleanup.push(bindHover(pinTab, { captureSidebarOnLeave: true, requireSidebarVisible: true }));
|
|
927
|
+
cleanup.push(bindHover(this.sidebar, { capturePinOnLeave: true }));
|
|
928
|
+
if (this._sidebarResizer) {
|
|
929
|
+
cleanup.push(bindHover(this._sidebarResizer, { captureSidebarOnLeave: true, capturePinOnLeave: true }));
|
|
930
|
+
}
|
|
931
|
+
cleanup.push(() => {
|
|
932
|
+
if (hoverUpdateRaf != null) cancelAnimationFrame(hoverUpdateRaf);
|
|
933
|
+
hoverUpdateRaf = null;
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const onPointerMove = (ev) => {
|
|
937
|
+
this._sidebarLastPointer = { x: ev.clientX, y: ev.clientY };
|
|
938
|
+
};
|
|
939
|
+
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
940
|
+
cleanup.push(() => window.removeEventListener('pointermove', onPointerMove));
|
|
941
|
+
|
|
942
|
+
const onTabClick = (ev) => {
|
|
943
|
+
try { ev.preventDefault(); ev.stopPropagation(); } catch { }
|
|
944
|
+
this._setSidebarPinned(!this._sidebarPinned);
|
|
945
|
+
};
|
|
946
|
+
pinTab.addEventListener('click', onTabClick);
|
|
947
|
+
cleanup.push(() => pinTab.removeEventListener('click', onTabClick));
|
|
948
|
+
|
|
949
|
+
const positionTab = () => this._positionSidebarPinTab();
|
|
950
|
+
window.addEventListener('resize', positionTab);
|
|
951
|
+
cleanup.push(() => window.removeEventListener('resize', positionTab));
|
|
952
|
+
|
|
953
|
+
let ro = null;
|
|
954
|
+
try {
|
|
955
|
+
if (window.ResizeObserver) {
|
|
956
|
+
ro = new ResizeObserver(() => positionTab());
|
|
957
|
+
ro.observe(this.sidebar);
|
|
958
|
+
}
|
|
959
|
+
} catch { /* ignore */ }
|
|
960
|
+
let mo = null;
|
|
961
|
+
try {
|
|
962
|
+
if (window.MutationObserver) {
|
|
963
|
+
mo = new MutationObserver(() => positionTab());
|
|
964
|
+
mo.observe(this.sidebar, { attributes: true, attributeFilter: ['style', 'hidden', 'class'] });
|
|
965
|
+
}
|
|
966
|
+
} catch { /* ignore */ }
|
|
967
|
+
cleanup.push(() => { try { ro && ro.disconnect(); } catch { } });
|
|
968
|
+
cleanup.push(() => { try { mo && mo.disconnect(); } catch { } });
|
|
969
|
+
|
|
970
|
+
this._sidebarDockCleanup = () => {
|
|
971
|
+
cleanup.forEach((fn) => { try { fn(); } catch { } });
|
|
972
|
+
cleanup.length = 0;
|
|
973
|
+
try { if (hoverStrip.parentNode) hoverStrip.parentNode.removeChild(hoverStrip); } catch { }
|
|
974
|
+
try { if (pinTab.parentNode) pinTab.parentNode.removeChild(pinTab); } catch { }
|
|
975
|
+
if (this._sidebarHoverStrip === hoverStrip) this._sidebarHoverStrip = null;
|
|
976
|
+
if (this._sidebarPinTab === pinTab) this._sidebarPinTab = null;
|
|
977
|
+
this._sidebarHoverTargets = null;
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
this._syncSidebarVisibility();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
_setSidebarAutoHideSuspended(suspended) {
|
|
984
|
+
const next = !!suspended;
|
|
985
|
+
if (this._sidebarAutoHideSuspended === next) return;
|
|
986
|
+
this._sidebarAutoHideSuspended = next;
|
|
987
|
+
this._syncSidebarVisibility();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
_setSidebarPinned(pinned) {
|
|
991
|
+
const next = !!pinned;
|
|
992
|
+
if (this._sidebarPinned === next) return;
|
|
993
|
+
this._sidebarPinned = next;
|
|
994
|
+
if (!next && this._sidebarAutoHideSuspended) {
|
|
995
|
+
// Allow explicit user collapse even when auto-hide is suspended (e.g. sketch mode).
|
|
996
|
+
this._sidebarAutoHideSuspended = false;
|
|
997
|
+
}
|
|
998
|
+
if (next) {
|
|
999
|
+
this._sidebarHoverVisible = false;
|
|
1000
|
+
} else {
|
|
1001
|
+
if (this._sidebarHoverTargets) this._sidebarHoverTargets.clear();
|
|
1002
|
+
this._sidebarHoverVisible = false;
|
|
1003
|
+
}
|
|
1004
|
+
this._syncSidebarVisibility();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
_setSidebarHoverVisible(visible) {
|
|
1008
|
+
const next = !!visible;
|
|
1009
|
+
if (this._sidebarHoverVisible === next) return;
|
|
1010
|
+
this._sidebarHoverVisible = next;
|
|
1011
|
+
this._syncSidebarVisibility();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
_refreshSidebarHoverTargetsFromPointer() {
|
|
1015
|
+
const targets = this._sidebarHoverTargets;
|
|
1016
|
+
const pos = this._sidebarLastPointer;
|
|
1017
|
+
if (!targets || !pos) return;
|
|
1018
|
+
targets.clear();
|
|
1019
|
+
const { x, y } = pos;
|
|
1020
|
+
const addIfHit = (el, requireVisible = false) => {
|
|
1021
|
+
if (!el) return;
|
|
1022
|
+
if (requireVisible && !this._isSidebarVisible()) return;
|
|
1023
|
+
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
1024
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
|
1025
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
1026
|
+
targets.add(el);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
addIfHit(this._sidebarHoverStrip);
|
|
1030
|
+
addIfHit(this._sidebarPinTab, true);
|
|
1031
|
+
addIfHit(this.sidebar, true);
|
|
1032
|
+
addIfHit(this._sidebarResizer, true);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
_getSidebarShouldShow() {
|
|
1036
|
+
if (!this.sidebar) return false;
|
|
1037
|
+
if (this._sidebarAutoHideSuspended) return true;
|
|
1038
|
+
if (this._sidebarPinned) return true;
|
|
1039
|
+
return !!this._sidebarHoverVisible;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
_isSidebarVisible() {
|
|
1043
|
+
if (!this.sidebar) return false;
|
|
1044
|
+
return !this._sidebarOffscreen
|
|
1045
|
+
&& !this.sidebar.hidden
|
|
1046
|
+
&& this.sidebar.style.display !== 'none'
|
|
1047
|
+
&& this.sidebar.style.visibility !== 'hidden';
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
_setSidebarElementVisible(visible) {
|
|
1051
|
+
if (!this.sidebar) return;
|
|
1052
|
+
const isVisible = this._isSidebarVisible();
|
|
1053
|
+
// Ensure the sidebar stays in the render tree even when collapsed.
|
|
1054
|
+
try { if (this.sidebar.hidden) this.sidebar.hidden = false; } catch { }
|
|
1055
|
+
if (this.sidebar.style.display === 'none') {
|
|
1056
|
+
if (this._sidebarStoredDisplay != null) {
|
|
1057
|
+
this.sidebar.style.display = this._sidebarStoredDisplay;
|
|
1058
|
+
} else {
|
|
1059
|
+
try { this.sidebar.style.removeProperty('display'); } catch { }
|
|
1060
|
+
this.sidebar.style.display = this.sidebar.style.display || '';
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (this.sidebar.style.visibility === 'hidden') {
|
|
1064
|
+
const visibility = this._sidebarStoredVisibility;
|
|
1065
|
+
this.sidebar.style.visibility = visibility && visibility !== 'hidden' ? visibility : 'visible';
|
|
1066
|
+
}
|
|
1067
|
+
if (visible) {
|
|
1068
|
+
if (!isVisible) {
|
|
1069
|
+
if (this._sidebarStoredTransform != null) {
|
|
1070
|
+
this.sidebar.style.transform = this._sidebarStoredTransform;
|
|
1071
|
+
} else {
|
|
1072
|
+
try { this.sidebar.style.removeProperty('transform'); } catch { }
|
|
1073
|
+
this.sidebar.style.transform = this.sidebar.style.transform || '';
|
|
1074
|
+
}
|
|
1075
|
+
if (this._sidebarStoredPointerEvents != null) {
|
|
1076
|
+
this.sidebar.style.pointerEvents = this._sidebarStoredPointerEvents;
|
|
1077
|
+
} else {
|
|
1078
|
+
try { this.sidebar.style.removeProperty('pointer-events'); } catch { }
|
|
1079
|
+
this.sidebar.style.pointerEvents = this.sidebar.style.pointerEvents || '';
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
this.sidebar.style.opacity = .9;
|
|
1083
|
+
this.sidebar.style.zIndex = String(7);
|
|
1084
|
+
this._sidebarOffscreen = false;
|
|
1085
|
+
} else {
|
|
1086
|
+
if (!this._sidebarOffscreen) {
|
|
1087
|
+
this._sidebarStoredDisplay = this.sidebar.style.display || '';
|
|
1088
|
+
this._sidebarStoredVisibility = this.sidebar.style.visibility || '';
|
|
1089
|
+
this._sidebarStoredTransform = this.sidebar.style.transform || '';
|
|
1090
|
+
this._sidebarStoredPointerEvents = this.sidebar.style.pointerEvents || '';
|
|
1091
|
+
}
|
|
1092
|
+
this.sidebar.style.transform = 'translateX(calc(-100% - 12px))';
|
|
1093
|
+
this.sidebar.style.pointerEvents = 'none';
|
|
1094
|
+
this._sidebarOffscreen = true;
|
|
1095
|
+
}
|
|
1096
|
+
try { this.mainToolbar?._positionWithSidebar?.(); } catch { }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
_updateSidebarDockUI() {
|
|
1100
|
+
const tab = this._sidebarPinTab;
|
|
1101
|
+
const strip = this._sidebarHoverStrip;
|
|
1102
|
+
const pinned = !!this._sidebarPinned;
|
|
1103
|
+
const hoverActive = !pinned && !this._sidebarAutoHideSuspended;
|
|
1104
|
+
if (tab) {
|
|
1105
|
+
tab.classList.toggle('is-pinned', pinned);
|
|
1106
|
+
tab.setAttribute('aria-pressed', pinned ? 'true' : 'false');
|
|
1107
|
+
tab.textContent = '📌';
|
|
1108
|
+
tab.title = pinned ? 'Collapse sidebar' : 'Pin sidebar';
|
|
1109
|
+
}
|
|
1110
|
+
if (strip) {
|
|
1111
|
+
strip.classList.toggle('is-active', hoverActive);
|
|
1112
|
+
strip.style.pointerEvents = hoverActive ? 'auto' : 'none';
|
|
1113
|
+
}
|
|
1114
|
+
this._positionSidebarPinTab();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
_positionSidebarPinTab() {
|
|
1118
|
+
const tab = this._sidebarPinTab;
|
|
1119
|
+
if (!tab) return;
|
|
1120
|
+
let left = 0;
|
|
1121
|
+
let top = 72;
|
|
1122
|
+
const rect = this.sidebar?.getBoundingClientRect?.();
|
|
1123
|
+
if (rect && rect.width > 0) {
|
|
1124
|
+
left = Math.max(0, Math.round(rect.right - 1));
|
|
1125
|
+
}
|
|
1126
|
+
if (rect && rect.height > 0) {
|
|
1127
|
+
const tabHeight = tab.getBoundingClientRect ? tab.getBoundingClientRect().height : tab.offsetHeight;
|
|
1128
|
+
const nextTop = rect.top + (rect.height - (tabHeight || 0)) / 2;
|
|
1129
|
+
if (Number.isFinite(nextTop)) top = Math.max(0, Math.round(nextTop));
|
|
1130
|
+
}
|
|
1131
|
+
tab.style.left = `${left}px`;
|
|
1132
|
+
tab.style.top = `${top}px`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
_syncSidebarVisibility() {
|
|
1136
|
+
const shouldShow = this._getSidebarShouldShow();
|
|
1137
|
+
this._setSidebarElementVisible(shouldShow);
|
|
1138
|
+
this._updateSidebarDockUI();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
async setupAccordion() {
|
|
1143
|
+
// Setup accordion
|
|
1144
|
+
this.accordion = await new AccordionWidget();
|
|
1145
|
+
await this.sidebar.appendChild(this.accordion.uiElement);
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
// Load saved plugins early (before File Manager autoloads last model)
|
|
1149
|
+
// Defer rendering of plugin side panels until proper placement later.
|
|
1150
|
+
try {
|
|
1151
|
+
await loadSavedPlugins(this);
|
|
1152
|
+
} catch (e) { console.warn('Plugin auto-load failed:', e); }
|
|
1153
|
+
|
|
1154
|
+
const fm = new FileManagerWidget(this);
|
|
1155
|
+
const fmSection = await this.accordion.addSection('File Manager');
|
|
1156
|
+
fmSection.uiElement.appendChild(fm.uiElement);
|
|
1157
|
+
// Expose for toolbar Save button
|
|
1158
|
+
this.fileManagerWidget = fm;
|
|
1159
|
+
|
|
1160
|
+
// Setup historyWidget
|
|
1161
|
+
this.historyWidget = await new HistoryWidget(this);
|
|
1162
|
+
this.partHistory.callbacks.run = async (featureID) => {
|
|
1163
|
+
//await this.historyWidget.renderHistory(featureID);
|
|
1164
|
+
};
|
|
1165
|
+
this.partHistory.callbacks.reset = async () => {
|
|
1166
|
+
//await this.historyWidget.reset();
|
|
1167
|
+
};
|
|
1168
|
+
this.partHistory.callbacks.afterRunHistory = () => {
|
|
1169
|
+
this._refreshAssemblyConstraintsPanelVisibility();
|
|
1170
|
+
this.applyMetadataColors();
|
|
1171
|
+
this._axisHelpersDirty = true;
|
|
1172
|
+
};
|
|
1173
|
+
this.partHistory.callbacks.afterReset = () => {
|
|
1174
|
+
this._refreshAssemblyConstraintsPanelVisibility();
|
|
1175
|
+
this.applyMetadataColors();
|
|
1176
|
+
this._axisHelpersDirty = true;
|
|
1177
|
+
};
|
|
1178
|
+
const historySection = await this.accordion.addSection("History");
|
|
1179
|
+
await historySection.uiElement.appendChild(await this.historyWidget.uiElement);
|
|
1180
|
+
|
|
1181
|
+
this.assemblyConstraintsWidget = new AssemblyConstraintsWidget(this);
|
|
1182
|
+
this._assemblyConstraintsSection = await this.accordion.addSection(ASSEMBLY_CONSTRAINTS_TITLE);
|
|
1183
|
+
this._assemblyConstraintsSection.uiElement.appendChild(this.assemblyConstraintsWidget.uiElement);
|
|
1184
|
+
|
|
1185
|
+
// setup expressions
|
|
1186
|
+
this.expressionsManager = await new expressionsManager(this);
|
|
1187
|
+
const expressionsSection = await this.accordion.addSection("Expressions");
|
|
1188
|
+
await expressionsSection.uiElement.appendChild(await this.expressionsManager.uiElement);
|
|
1189
|
+
|
|
1190
|
+
// Setup sceneManagerUi
|
|
1191
|
+
this.sceneManagerUi = await new SceneListing(this.scene, {
|
|
1192
|
+
onSelection: (obj) => this._applySelectionTarget(obj, { triggerOnClick: false, allowDiagnostics: false }),
|
|
1193
|
+
});
|
|
1194
|
+
const sceneSection = await this.accordion.addSection("Scene Manager");
|
|
1195
|
+
await sceneSection.uiElement.appendChild(this.sceneManagerUi.uiElement);
|
|
1196
|
+
|
|
1197
|
+
// PMI Views (saved camera snapshots)
|
|
1198
|
+
this.pmiViewsWidget = new PMIViewsWidget(this);
|
|
1199
|
+
const pmiViewsSection = await this.accordion.addSection("PMI Views");
|
|
1200
|
+
pmiViewsSection.uiElement.appendChild(this.pmiViewsWidget.uiElement);
|
|
1201
|
+
|
|
1202
|
+
// CADmaterials (Settings panel)
|
|
1203
|
+
this.cadMaterialsUi = await new CADmaterialWidget(this);
|
|
1204
|
+
const displaySection = await this.accordion.addSection("Display Settings");
|
|
1205
|
+
await displaySection.uiElement.appendChild(this.cadMaterialsUi.uiElement);
|
|
1206
|
+
|
|
1207
|
+
// From this point on, plugin UI can be added immediately,
|
|
1208
|
+
// and should be inserted just before the "Display Settings" panel.
|
|
1209
|
+
this._pluginUiReady = true;
|
|
1210
|
+
|
|
1211
|
+
// Drain any queued plugin side panels so they appear immediately before settings
|
|
1212
|
+
try {
|
|
1213
|
+
const q = Array.isArray(this._pendingSidePanels) ? this._pendingSidePanels : [];
|
|
1214
|
+
this._pendingSidePanels = [];
|
|
1215
|
+
for (const it of q) {
|
|
1216
|
+
try { await this._applyPluginSidePanel(it); } catch { }
|
|
1217
|
+
}
|
|
1218
|
+
} catch { }
|
|
1219
|
+
|
|
1220
|
+
// Plugin setup panel (after settings)
|
|
1221
|
+
const pluginsSection = await this.accordion.addSection('Plugins');
|
|
1222
|
+
const pluginsWidget = new PluginsWidget(this);
|
|
1223
|
+
pluginsSection.uiElement.appendChild(pluginsWidget.uiElement);
|
|
1224
|
+
|
|
1225
|
+
await this.accordion.collapseAll();
|
|
1226
|
+
await this.accordion.expandSection("Scene Manager");
|
|
1227
|
+
|
|
1228
|
+
await this.accordion.expandSection("History");
|
|
1229
|
+
const hasAssemblyComponents = !!this.partHistory?.hasAssemblyComponents?.();
|
|
1230
|
+
if (hasAssemblyComponents) {
|
|
1231
|
+
await this.accordion.expandSection(ASSEMBLY_CONSTRAINTS_TITLE);
|
|
1232
|
+
}
|
|
1233
|
+
await this.accordion.expandSection("PMI Views");
|
|
1234
|
+
|
|
1235
|
+
this._refreshAssemblyConstraintsPanelVisibility();
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
// Mount the main toolbar (layout only; buttons registered externally)
|
|
1239
|
+
this.mainToolbar = new MainToolbar(this);
|
|
1240
|
+
// Register core/default toolbar buttons via the public API
|
|
1241
|
+
try { registerDefaultToolbarButtons(this); } catch { }
|
|
1242
|
+
// Drain any queued custom toolbar buttons from early plugin registration
|
|
1243
|
+
try {
|
|
1244
|
+
const q = Array.isArray(this._pendingToolbarButtons) ? this._pendingToolbarButtons : [];
|
|
1245
|
+
this._pendingToolbarButtons = [];
|
|
1246
|
+
for (const it of q) {
|
|
1247
|
+
try { this.mainToolbar.addCustomButton(it); } catch { }
|
|
1248
|
+
}
|
|
1249
|
+
} catch { }
|
|
1250
|
+
|
|
1251
|
+
// Ensure toolbar sits above the canvas and doesn't block controls when not hovered
|
|
1252
|
+
try { this.renderer.domElement.style.marginTop = '0px'; } catch { }
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Public: allow plugins to add toolbar buttons even before MainToolbar is constructed
|
|
1256
|
+
addToolbarButton(label, title, onClick) {
|
|
1257
|
+
const item = { label, title, onClick };
|
|
1258
|
+
if (this.mainToolbar && typeof this.mainToolbar.addCustomButton === 'function') {
|
|
1259
|
+
try { return this.mainToolbar.addCustomButton(item); } catch { return null; }
|
|
1260
|
+
}
|
|
1261
|
+
this._pendingToolbarButtons = this._pendingToolbarButtons || [];
|
|
1262
|
+
this._pendingToolbarButtons.push(item);
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
_syncHistoryUiAfterUndoRedo() {
|
|
1267
|
+
try {
|
|
1268
|
+
if (this.expressionsManager?.textArea) {
|
|
1269
|
+
this.expressionsManager.textArea.value = this.partHistory?.expressions || '';
|
|
1270
|
+
}
|
|
1271
|
+
} catch { }
|
|
1272
|
+
try {
|
|
1273
|
+
if (this.pmiViewsWidget) {
|
|
1274
|
+
this.pmiViewsWidget.refreshFromHistory?.();
|
|
1275
|
+
this.pmiViewsWidget._renderList?.();
|
|
1276
|
+
}
|
|
1277
|
+
} catch { }
|
|
1278
|
+
try { this.historyWidget?.render?.(); } catch { }
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async _runFeatureHistoryUndoRedo(direction) {
|
|
1282
|
+
const ph = this.partHistory;
|
|
1283
|
+
if (!ph) return false;
|
|
1284
|
+
let changed = false;
|
|
1285
|
+
try {
|
|
1286
|
+
if (direction === 'redo') changed = await ph.redoFeatureHistory();
|
|
1287
|
+
else changed = await ph.undoFeatureHistory();
|
|
1288
|
+
} catch { }
|
|
1289
|
+
try { this._syncHistoryUiAfterUndoRedo(); } catch { }
|
|
1290
|
+
return changed;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Apply a single queued plugin side panel entry
|
|
1294
|
+
async _applyPluginSidePanel({ title, content }) {
|
|
1295
|
+
if (!this.accordion || typeof this.accordion.addSection !== 'function') return null;
|
|
1296
|
+
const t = String(title || 'Plugin');
|
|
1297
|
+
const sec = await this.accordion.addSection(t);
|
|
1298
|
+
if (!sec) return null;
|
|
1299
|
+
try {
|
|
1300
|
+
if (typeof content === 'function') {
|
|
1301
|
+
const el = await content();
|
|
1302
|
+
if (el) sec.uiElement.appendChild(el);
|
|
1303
|
+
} else if (content instanceof HTMLElement) {
|
|
1304
|
+
sec.uiElement.appendChild(content);
|
|
1305
|
+
} else if (content != null) {
|
|
1306
|
+
const pre = document.createElement('pre');
|
|
1307
|
+
pre.textContent = String(content);
|
|
1308
|
+
sec.uiElement.appendChild(pre);
|
|
1309
|
+
}
|
|
1310
|
+
// Reposition this plugin section to immediately before the Display Settings panel, if present
|
|
1311
|
+
try {
|
|
1312
|
+
const root = this.accordion.uiElement;
|
|
1313
|
+
const targetTitle = root.querySelector('.accordion-title[name="accordion-title-Display Settings"]');
|
|
1314
|
+
if (targetTitle) {
|
|
1315
|
+
const secTitle = root.querySelector(`.accordion-title[name="accordion-title-${t}"]`);
|
|
1316
|
+
if (secTitle && sec.uiElement && secTitle !== targetTitle) {
|
|
1317
|
+
root.insertBefore(secTitle, targetTitle);
|
|
1318
|
+
root.insertBefore(sec.uiElement, targetTitle);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
} catch { }
|
|
1322
|
+
} catch { }
|
|
1323
|
+
return sec;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Public: allow plugins to register side panels; queued until core UI/toolbar are ready
|
|
1327
|
+
async addPluginSidePanel(title, content) {
|
|
1328
|
+
const item = { title, content };
|
|
1329
|
+
if (this._pluginUiReady) {
|
|
1330
|
+
try { return await this._applyPluginSidePanel(item); } catch { return null; }
|
|
1331
|
+
}
|
|
1332
|
+
this._pendingSidePanels = this._pendingSidePanels || [];
|
|
1333
|
+
this._pendingSidePanels.push(item);
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
_refreshAssemblyConstraintsPanelVisibility() {
|
|
1338
|
+
if (!this.accordion || !this.accordion.uiElement) return;
|
|
1339
|
+
const shouldShow = !!this.partHistory?.hasAssemblyComponents?.();
|
|
1340
|
+
const prevVisible = this._assemblyConstraintsVisible;
|
|
1341
|
+
this._assemblyConstraintsVisible = shouldShow;
|
|
1342
|
+
|
|
1343
|
+
if (shouldShow) {
|
|
1344
|
+
this.accordion.showSection?.(ASSEMBLY_CONSTRAINTS_TITLE);
|
|
1345
|
+
if (prevVisible === false) {
|
|
1346
|
+
try { this.accordion.expandSection?.(ASSEMBLY_CONSTRAINTS_TITLE); } catch { /* ignore */ }
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
const applied = this.accordion.hideSection?.(ASSEMBLY_CONSTRAINTS_TITLE);
|
|
1350
|
+
if (!applied) {
|
|
1351
|
+
// Retry once after next paint in case the nodes weren't available yet.
|
|
1352
|
+
setTimeout(() => {
|
|
1353
|
+
try { this.accordion.hideSection?.(ASSEMBLY_CONSTRAINTS_TITLE); } catch { /* ignore */ }
|
|
1354
|
+
}, 0);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (prevVisible !== shouldShow) {
|
|
1359
|
+
// No-op; kept for future hooks
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// ----------------------------------------
|
|
1364
|
+
// Public API
|
|
1365
|
+
// ----------------------------------------
|
|
1366
|
+
dispose() {
|
|
1367
|
+
if (this._disposed) return;
|
|
1368
|
+
this._disposed = true;
|
|
1369
|
+
cancelAnimationFrame(this._raf);
|
|
1370
|
+
try { this._stopComponentTransformSession(); } catch { }
|
|
1371
|
+
try { this._sidebarResizerCleanup?.(); } catch { }
|
|
1372
|
+
try { this._sidebarDockCleanup?.(); } catch { }
|
|
1373
|
+
const el = this.renderer?.domElement;
|
|
1374
|
+
this._detachRendererEvents(el);
|
|
1375
|
+
window.removeEventListener('pointerup', this._onPointerUp, { capture: true });
|
|
1376
|
+
document.removeEventListener('dblclick', this._onGlobalDoubleClick, { capture: true });
|
|
1377
|
+
window.removeEventListener('resize', this._onResize);
|
|
1378
|
+
window.removeEventListener('keydown', this._onKeyDown, { passive: false });
|
|
1379
|
+
this.controls?.dispose?.();
|
|
1380
|
+
this.renderer?.dispose?.();
|
|
1381
|
+
if (this._webglRenderer && this._webglRenderer !== this.renderer) {
|
|
1382
|
+
try { this._webglRenderer.dispose(); } catch { }
|
|
1383
|
+
}
|
|
1384
|
+
try { if (this._sketchMode) this._sketchMode.dispose(); } catch { }
|
|
1385
|
+
try { if (this._splineMode) this._splineMode.dispose(); } catch { }
|
|
1386
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// ----------------------------------------
|
|
1390
|
+
// Sketch Mode API
|
|
1391
|
+
// ----------------------------------------
|
|
1392
|
+
startSketchMode(featureID) {
|
|
1393
|
+
// Hide the sketch in the scene if it exists
|
|
1394
|
+
try {
|
|
1395
|
+
const ph = this.partHistory.getObjectByName(featureID);
|
|
1396
|
+
if (ph) ph.visible = false;
|
|
1397
|
+
} catch (e) {
|
|
1398
|
+
debugLog(e);
|
|
1399
|
+
debugLog(this.viewer);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
debugLog('Starting Sketch Mode for featureID:', featureID);
|
|
1403
|
+
debugLog(this.partHistory.scene);
|
|
1404
|
+
debugLog(this.partHistory);
|
|
1405
|
+
debugLog(this);
|
|
1406
|
+
|
|
1407
|
+
try { if (this._sketchMode) this._sketchMode.dispose(); } catch { }
|
|
1408
|
+
this._setSidebarAutoHideSuspended(true);
|
|
1409
|
+
this._sketchMode = new SketchMode3D(this, featureID);
|
|
1410
|
+
this._sketchMode.open();
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
onSketchFinished(featureID, sketchObject) {
|
|
1416
|
+
const ph = this.partHistory;
|
|
1417
|
+
if (!ph || !featureID) return;
|
|
1418
|
+
// Always restore normal UI first
|
|
1419
|
+
this.endSketchMode();
|
|
1420
|
+
const f = Array.isArray(ph.features) ? ph.features.find(x => x?.inputParams?.featureID === featureID) : null;
|
|
1421
|
+
if (!f) return;
|
|
1422
|
+
f.lastRunInputParams = {};
|
|
1423
|
+
f.timestamp = 0;
|
|
1424
|
+
f.dirty = true;
|
|
1425
|
+
f.persistentData = f.persistentData || {};
|
|
1426
|
+
f.persistentData.sketch = sketchObject || {};
|
|
1427
|
+
// re-run to keep downstream in sync (even if SketchFeature.run has no output yet)
|
|
1428
|
+
try {
|
|
1429
|
+
const runPromise = ph.runHistory();
|
|
1430
|
+
if (runPromise && typeof runPromise.then === 'function') {
|
|
1431
|
+
runPromise.then(() => ph.queueHistorySnapshot?.({ debounceMs: 0, reason: 'sketch' }));
|
|
1432
|
+
} else {
|
|
1433
|
+
ph.queueHistorySnapshot?.({ debounceMs: 0, reason: 'sketch' });
|
|
1434
|
+
}
|
|
1435
|
+
} catch { }
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
onSketchCancelled(_featureID) {
|
|
1439
|
+
this.endSketchMode();
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
endSketchMode() {
|
|
1443
|
+
try { if (this._sketchMode) this._sketchMode.close(); } catch { }
|
|
1444
|
+
this._sketchMode = null;
|
|
1445
|
+
// Ensure core UI is visible and controls enabled
|
|
1446
|
+
try { this._setSidebarAutoHideSuspended(false); } catch { }
|
|
1447
|
+
try { if (this.controls) this.controls.enabled = true; } catch { }
|
|
1448
|
+
|
|
1449
|
+
// Clean up any legacy overlays that might still be mounted (from old 2D mode)
|
|
1450
|
+
try {
|
|
1451
|
+
const c = this.container;
|
|
1452
|
+
if (c && typeof c.querySelectorAll === 'function') {
|
|
1453
|
+
const leftovers = c.querySelectorAll('.sketch-overlay');
|
|
1454
|
+
leftovers.forEach(el => { try { el.parentNode && el.parentNode.removeChild(el); } catch { } });
|
|
1455
|
+
}
|
|
1456
|
+
} catch { }
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ----------------------------------------
|
|
1460
|
+
// Spline Mode API
|
|
1461
|
+
// ----------------------------------------
|
|
1462
|
+
startSplineMode(splineSession) {
|
|
1463
|
+
debugLog('Starting Spline Mode for session:', splineSession);
|
|
1464
|
+
this._splineMode = splineSession;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
endSplineMode() {
|
|
1468
|
+
debugLog('Ending Spline Mode');
|
|
1469
|
+
this._splineMode = null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ----------------------------------------
|
|
1473
|
+
// PMI Edit Mode API
|
|
1474
|
+
// ----------------------------------------
|
|
1475
|
+
startPMIMode(viewEntry, viewIndex, widget = this.pmiViewsWidget) {
|
|
1476
|
+
const alreadyActive = !!this._pmiMode;
|
|
1477
|
+
if (!alreadyActive) {
|
|
1478
|
+
try { this.assemblyConstraintsWidget?.onPMIModeEnter?.(); } catch { }
|
|
1479
|
+
}
|
|
1480
|
+
try { if (this._pmiMode) this._pmiMode.dispose(); } catch { }
|
|
1481
|
+
try {
|
|
1482
|
+
if (!alreadyActive) this._setSidebarAutoHideSuspended(true);
|
|
1483
|
+
this._pmiMode = new PMIMode(this, viewEntry, viewIndex, widget);
|
|
1484
|
+
this._pmiMode.open();
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
this._pmiMode = null;
|
|
1487
|
+
if (!alreadyActive) {
|
|
1488
|
+
try { this.assemblyConstraintsWidget?.onPMIModeExit?.(); } catch { }
|
|
1489
|
+
try { this._setSidebarAutoHideSuspended(false); } catch { }
|
|
1490
|
+
}
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
onPMIFinished(_updatedView) {
|
|
1496
|
+
this.endPMIMode();
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
onPMICancelled() {
|
|
1500
|
+
this.endPMIMode();
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
endPMIMode() {
|
|
1504
|
+
const hadMode = !!this._pmiMode;
|
|
1505
|
+
try { if (this._pmiMode) this._pmiMode.dispose(); } catch { }
|
|
1506
|
+
this._pmiMode = null;
|
|
1507
|
+
if (hadMode) {
|
|
1508
|
+
try { this.assemblyConstraintsWidget?.onPMIModeExit?.(); } catch { }
|
|
1509
|
+
}
|
|
1510
|
+
// Robustly restore core UI similar to endSketchMode
|
|
1511
|
+
try { this._setSidebarAutoHideSuspended(false); } catch { }
|
|
1512
|
+
try { if (this.controls) this.controls.enabled = true; } catch { }
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
render() {
|
|
1516
|
+
// Keep the camera (and its attached light) anchored in the scene
|
|
1517
|
+
if (this.camera && this.camera.parent !== this.scene) {
|
|
1518
|
+
try { this.scene.add(this.camera); } catch { /* ignore add errors */ }
|
|
1519
|
+
}
|
|
1520
|
+
this._updateAxisHelpers();
|
|
1521
|
+
this._updateCameraLightRig();
|
|
1522
|
+
this._updateDepthRange();
|
|
1523
|
+
if (this._rendererMode === 'svg') {
|
|
1524
|
+
this._renderSvgScene();
|
|
1525
|
+
} else {
|
|
1526
|
+
this.renderer.render(this.scene, this.camera);
|
|
1527
|
+
try { this.viewCube && this.viewCube.render(); } catch { }
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
_renderSvgScene() {
|
|
1532
|
+
if (!this.renderer || !this.scene || !this.camera) return;
|
|
1533
|
+
const el = this.renderer.domElement;
|
|
1534
|
+
if (!el) return;
|
|
1535
|
+
try { this.scene.updateMatrixWorld(true); } catch { }
|
|
1536
|
+
try { this.camera.updateMatrixWorld?.(); } catch { }
|
|
1537
|
+
this._resizeRendererToDisplaySize();
|
|
1538
|
+
|
|
1539
|
+
const rect = el.getBoundingClientRect();
|
|
1540
|
+
const width = Math.max(1, Math.floor(rect.width || this.container?.clientWidth || 0));
|
|
1541
|
+
const height = Math.max(1, Math.floor(rect.height || this.container?.clientHeight || 0));
|
|
1542
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 1 || height <= 1) return;
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
if (typeof this.renderer.setClearColor === 'function') {
|
|
1546
|
+
this.renderer.setClearColor(this._clearColor);
|
|
1547
|
+
}
|
|
1548
|
+
} catch { }
|
|
1549
|
+
|
|
1550
|
+
const pointAdjustments = [];
|
|
1551
|
+
const sideAdjustments = [];
|
|
1552
|
+
const tempLines = [];
|
|
1553
|
+
const tempGroup = new THREE.Group();
|
|
1554
|
+
const hiddenLines = [];
|
|
1555
|
+
try {
|
|
1556
|
+
if (this.camera?.isOrthographicCamera) {
|
|
1557
|
+
const span = (Number(this.camera.right) - Number(this.camera.left)) || 0;
|
|
1558
|
+
if (Number.isFinite(span) && span > 0) {
|
|
1559
|
+
const scaleFactor = span / width;
|
|
1560
|
+
this.scene.traverse((obj) => {
|
|
1561
|
+
if (!obj?.isPoints) return;
|
|
1562
|
+
const mat = obj.material;
|
|
1563
|
+
if (Array.isArray(mat)) {
|
|
1564
|
+
for (const m of mat) {
|
|
1565
|
+
if (!m?.isPointsMaterial || !Number.isFinite(m.size)) continue;
|
|
1566
|
+
pointAdjustments.push([m, m.size]);
|
|
1567
|
+
m.size = m.size * scaleFactor;
|
|
1568
|
+
}
|
|
1569
|
+
} else if (mat?.isPointsMaterial && Number.isFinite(mat.size)) {
|
|
1570
|
+
pointAdjustments.push([mat, mat.size]);
|
|
1571
|
+
mat.size = mat.size * scaleFactor;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const occluders = this._collectSvgOccluders(sideAdjustments);
|
|
1578
|
+
const raycaster = this._svgRaycaster || new THREE.Raycaster();
|
|
1579
|
+
this._svgRaycaster = raycaster;
|
|
1580
|
+
const occlusionEps = this._computeSvgOcclusionEps();
|
|
1581
|
+
|
|
1582
|
+
this.scene.traverse((obj) => {
|
|
1583
|
+
if (!obj?.visible) return;
|
|
1584
|
+
if (!obj.isLine2 && !obj.isLineSegments2) return;
|
|
1585
|
+
const line = this._buildSvgLineFromLine2(obj, {
|
|
1586
|
+
camera: this.camera,
|
|
1587
|
+
occluders,
|
|
1588
|
+
raycaster,
|
|
1589
|
+
occlusionEps,
|
|
1590
|
+
});
|
|
1591
|
+
if (!line) return;
|
|
1592
|
+
tempLines.push(line);
|
|
1593
|
+
tempGroup.add(line);
|
|
1594
|
+
hiddenLines.push([obj, obj.visible]);
|
|
1595
|
+
obj.visible = false;
|
|
1596
|
+
});
|
|
1597
|
+
if (tempLines.length) {
|
|
1598
|
+
this.scene.add(tempGroup);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
this._restoreSvgMaterialSides(sideAdjustments);
|
|
1602
|
+
|
|
1603
|
+
this.renderer.render(this.scene, this.camera);
|
|
1604
|
+
try { el.style.background = this._clearAlpha === 0 ? 'transparent' : this._clearColor.getStyle(); } catch { }
|
|
1605
|
+
} catch { } finally {
|
|
1606
|
+
try {
|
|
1607
|
+
if (tempLines.length) {
|
|
1608
|
+
this.scene.remove(tempGroup);
|
|
1609
|
+
for (const line of tempLines) {
|
|
1610
|
+
try { line.geometry?.dispose?.(); } catch { }
|
|
1611
|
+
try { line.material?.dispose?.(); } catch { }
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
} catch { }
|
|
1615
|
+
for (const [obj, wasVisible] of hiddenLines) {
|
|
1616
|
+
try { obj.visible = wasVisible; } catch { }
|
|
1617
|
+
}
|
|
1618
|
+
this._restoreSvgMaterialSides(sideAdjustments);
|
|
1619
|
+
for (const [mat, size] of pointAdjustments) {
|
|
1620
|
+
try { mat.size = size; } catch { }
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
_buildSvgLineFromLine2(obj, { camera, occluders, raycaster, occlusionEps } = {}) {
|
|
1626
|
+
const geom = obj.geometry;
|
|
1627
|
+
const start = geom?.attributes?.instanceStart;
|
|
1628
|
+
const end = geom?.attributes?.instanceEnd;
|
|
1629
|
+
let positions = null;
|
|
1630
|
+
if (start && end && Number.isFinite(start.count) && start.count > 0) {
|
|
1631
|
+
const count = Math.min(start.count, end.count);
|
|
1632
|
+
positions = new Float32Array(count * 6);
|
|
1633
|
+
for (let i = 0; i < count; i += 1) {
|
|
1634
|
+
positions[i * 6] = start.getX(i);
|
|
1635
|
+
positions[i * 6 + 1] = start.getY(i);
|
|
1636
|
+
positions[i * 6 + 2] = start.getZ(i);
|
|
1637
|
+
positions[i * 6 + 3] = end.getX(i);
|
|
1638
|
+
positions[i * 6 + 4] = end.getY(i);
|
|
1639
|
+
positions[i * 6 + 5] = end.getZ(i);
|
|
1640
|
+
}
|
|
1641
|
+
} else if (geom?.attributes?.position?.count >= 2) {
|
|
1642
|
+
const pos = geom.attributes.position;
|
|
1643
|
+
const segCount = pos.count - 1;
|
|
1644
|
+
positions = new Float32Array(segCount * 6);
|
|
1645
|
+
for (let i = 0; i < segCount; i += 1) {
|
|
1646
|
+
positions[i * 6] = pos.getX(i);
|
|
1647
|
+
positions[i * 6 + 1] = pos.getY(i);
|
|
1648
|
+
positions[i * 6 + 2] = pos.getZ(i);
|
|
1649
|
+
positions[i * 6 + 3] = pos.getX(i + 1);
|
|
1650
|
+
positions[i * 6 + 4] = pos.getY(i + 1);
|
|
1651
|
+
positions[i * 6 + 5] = pos.getZ(i + 1);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (!positions || positions.length < 6) return null;
|
|
1656
|
+
|
|
1657
|
+
const material = Array.isArray(obj.material) ? obj.material[0] : obj.material;
|
|
1658
|
+
const wantsOcclusion = material?.depthTest !== false
|
|
1659
|
+
&& obj?.type === 'EDGE'
|
|
1660
|
+
&& Array.isArray(occluders)
|
|
1661
|
+
&& occluders.length
|
|
1662
|
+
&& camera
|
|
1663
|
+
&& raycaster;
|
|
1664
|
+
|
|
1665
|
+
if (wantsOcclusion) {
|
|
1666
|
+
const edgeFaces = Array.isArray(obj.faces) ? new Set(obj.faces) : null;
|
|
1667
|
+
const w1 = this._svgTmpVecA || (this._svgTmpVecA = new THREE.Vector3());
|
|
1668
|
+
const w2 = this._svgTmpVecB || (this._svgTmpVecB = new THREE.Vector3());
|
|
1669
|
+
const visible = [];
|
|
1670
|
+
for (let i = 0; i < positions.length; i += 6) {
|
|
1671
|
+
w1.set(positions[i], positions[i + 1], positions[i + 2]).applyMatrix4(obj.matrixWorld);
|
|
1672
|
+
w2.set(positions[i + 3], positions[i + 4], positions[i + 5]).applyMatrix4(obj.matrixWorld);
|
|
1673
|
+
if (this._isSvgSegmentVisible(w1, w2, camera, raycaster, occluders, edgeFaces, occlusionEps)) {
|
|
1674
|
+
visible.push(
|
|
1675
|
+
positions[i], positions[i + 1], positions[i + 2],
|
|
1676
|
+
positions[i + 3], positions[i + 4], positions[i + 5]
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (!visible.length) return null;
|
|
1681
|
+
positions = new Float32Array(visible);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const geomOut = new THREE.BufferGeometry();
|
|
1685
|
+
geomOut.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1686
|
+
|
|
1687
|
+
const color = material?.color ? material.color : new THREE.Color('#ffffff');
|
|
1688
|
+
const opacity = Number.isFinite(material?.opacity) ? material.opacity : 1;
|
|
1689
|
+
const transparent = Boolean(material?.transparent) || opacity < 1;
|
|
1690
|
+
const linewidth = Number.isFinite(material?.linewidth) ? material.linewidth : 1;
|
|
1691
|
+
let matOut = null;
|
|
1692
|
+
|
|
1693
|
+
if (material?.dashed || material?.isLineDashedMaterial) {
|
|
1694
|
+
matOut = new THREE.LineDashedMaterial({
|
|
1695
|
+
color,
|
|
1696
|
+
linewidth,
|
|
1697
|
+
transparent,
|
|
1698
|
+
opacity,
|
|
1699
|
+
dashSize: Number.isFinite(material?.dashSize) ? material.dashSize : 0.5,
|
|
1700
|
+
gapSize: Number.isFinite(material?.gapSize) ? material.gapSize : 0.5,
|
|
1701
|
+
});
|
|
1702
|
+
} else {
|
|
1703
|
+
matOut = new THREE.LineBasicMaterial({
|
|
1704
|
+
color,
|
|
1705
|
+
linewidth,
|
|
1706
|
+
transparent,
|
|
1707
|
+
opacity,
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const line = new THREE.LineSegments(geomOut, matOut);
|
|
1712
|
+
line.matrixAutoUpdate = false;
|
|
1713
|
+
try { line.matrix.copy(obj.matrixWorld); } catch { }
|
|
1714
|
+
try { line.matrixWorld.copy(obj.matrixWorld); } catch { }
|
|
1715
|
+
line.renderOrder = 2;
|
|
1716
|
+
line.visible = true;
|
|
1717
|
+
if (matOut.isLineDashedMaterial) {
|
|
1718
|
+
try { line.computeLineDistances(); } catch { }
|
|
1719
|
+
}
|
|
1720
|
+
return line;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
_collectSvgOccluders(sideAdjustments) {
|
|
1724
|
+
const occluders = [];
|
|
1725
|
+
try {
|
|
1726
|
+
this.scene.traverse((obj) => {
|
|
1727
|
+
if (!obj?.visible || !obj.isMesh) return;
|
|
1728
|
+
if (obj.type && obj.type !== 'FACE') return;
|
|
1729
|
+
const mat = obj.material;
|
|
1730
|
+
const mats = Array.isArray(mat) ? mat : [mat];
|
|
1731
|
+
if (!mats.some((m) => m && m.opacity !== 0)) return;
|
|
1732
|
+
if (Array.isArray(sideAdjustments)) {
|
|
1733
|
+
for (const m of mats) {
|
|
1734
|
+
if (!m || m.side === THREE.DoubleSide) continue;
|
|
1735
|
+
sideAdjustments.push([m, m.side]);
|
|
1736
|
+
m.side = THREE.DoubleSide;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
occluders.push(obj);
|
|
1740
|
+
});
|
|
1741
|
+
} catch { }
|
|
1742
|
+
return occluders;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
_restoreSvgMaterialSides(sideAdjustments) {
|
|
1746
|
+
if (!Array.isArray(sideAdjustments) || !sideAdjustments.length) return;
|
|
1747
|
+
for (const [mat, side] of sideAdjustments) {
|
|
1748
|
+
if (!mat) continue;
|
|
1749
|
+
try { mat.side = side; } catch { }
|
|
1750
|
+
}
|
|
1751
|
+
sideAdjustments.length = 0;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
_computeSvgOcclusionEps() {
|
|
1755
|
+
const cam = this.camera;
|
|
1756
|
+
if (!cam) return 1e-4;
|
|
1757
|
+
if (cam.isOrthographicCamera) {
|
|
1758
|
+
const span = Math.abs(Number(cam.right) - Number(cam.left)) || 0;
|
|
1759
|
+
return Math.max(1e-4, span * 1e-4);
|
|
1760
|
+
}
|
|
1761
|
+
const target = this.controls?.target;
|
|
1762
|
+
const dist = (target && cam.position?.distanceTo?.(target)) || cam.position?.length?.() || 1;
|
|
1763
|
+
return Math.max(1e-4, dist * 1e-4);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
_isSvgSegmentVisible(a, b, camera, raycaster, occluders, edgeFaces, eps) {
|
|
1767
|
+
if (!camera || !raycaster || !Array.isArray(occluders) || !occluders.length) return true;
|
|
1768
|
+
const samples = this._svgEdgeSamples || (this._svgEdgeSamples = [0.2, 0.5, 0.8]);
|
|
1769
|
+
const p = this._svgTmpVecC || (this._svgTmpVecC = new THREE.Vector3());
|
|
1770
|
+
for (const t of samples) {
|
|
1771
|
+
p.lerpVectors(a, b, t);
|
|
1772
|
+
if (!this._isSvgPointOccluded(p, camera, raycaster, occluders, edgeFaces, eps)) return true;
|
|
1773
|
+
}
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
_isSvgPointOccluded(point, camera, raycaster, occluders, edgeFaces, eps) {
|
|
1778
|
+
const ndc = this._svgTmpVecD || (this._svgTmpVecD = new THREE.Vector3());
|
|
1779
|
+
ndc.copy(point).project(camera);
|
|
1780
|
+
if (!Number.isFinite(ndc.x) || !Number.isFinite(ndc.y) || !Number.isFinite(ndc.z)) return false;
|
|
1781
|
+
if (ndc.z < -1 || ndc.z > 1) return true;
|
|
1782
|
+
raycaster.setFromCamera({ x: ndc.x, y: ndc.y }, camera);
|
|
1783
|
+
const dist = raycaster.ray.origin.distanceTo(point);
|
|
1784
|
+
const pad = Number.isFinite(eps) ? eps : 1e-4;
|
|
1785
|
+
raycaster.near = 0;
|
|
1786
|
+
raycaster.far = Math.max(0, dist - pad);
|
|
1787
|
+
const hits = raycaster.intersectObjects(occluders, true);
|
|
1788
|
+
if (!hits.length) return false;
|
|
1789
|
+
if (edgeFaces && edgeFaces.size) {
|
|
1790
|
+
for (const hit of hits) {
|
|
1791
|
+
if (!this._isSvgHitFromEdgeFace(hit, edgeFaces)) return true;
|
|
1792
|
+
}
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
_isSvgHitFromEdgeFace(hit, edgeFaces) {
|
|
1799
|
+
let obj = hit?.object || null;
|
|
1800
|
+
for (let i = 0; i < 3 && obj; i += 1) {
|
|
1801
|
+
if (edgeFaces.has(obj)) return true;
|
|
1802
|
+
obj = obj.parent || null;
|
|
1803
|
+
}
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
_updateCameraLightRig() {
|
|
1808
|
+
if (!this._cameraLightRig || !this.camera || !this.renderer) return;
|
|
1809
|
+
const { pointLights, lightDirections, baseLightRadius } = this._cameraLightRig;
|
|
1810
|
+
if (!pointLights?.length || !lightDirections?.length) return;
|
|
1811
|
+
const sizeVec = this.renderer.getSize ? this.renderer.getSize(new THREE.Vector2()) : null;
|
|
1812
|
+
const width = sizeVec?.width || this.renderer?.domElement?.clientWidth || 0;
|
|
1813
|
+
const height = sizeVec?.height || this.renderer?.domElement?.clientHeight || 0;
|
|
1814
|
+
if (!width || !height) return;
|
|
1815
|
+
|
|
1816
|
+
const wpp = this._worldPerPixel(this.camera, width, height);
|
|
1817
|
+
const screenDiagonal = Math.sqrt(width * width + height * height);
|
|
1818
|
+
// Scale radius with visible span so lights spread further when zoomed out and stay even when zoomed in
|
|
1819
|
+
const radius = Math.max(baseLightRadius, wpp * screenDiagonal * 1.4);
|
|
1820
|
+
|
|
1821
|
+
pointLights.forEach((light, idx) => {
|
|
1822
|
+
const dir = lightDirections[idx] || [0, 0, 0];
|
|
1823
|
+
light.position.set(dir[0] * radius, dir[1] * radius, dir[2] * radius);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
_collectAxisHelpers() {
|
|
1828
|
+
this._axisHelpers = new Set();
|
|
1829
|
+
if (!this.scene || typeof this.scene.traverse !== 'function') {
|
|
1830
|
+
this._axisHelpersDirty = false;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
this.scene.traverse((obj) => {
|
|
1834
|
+
if (obj?.userData?.axisHelper) this._axisHelpers.add(obj);
|
|
1835
|
+
});
|
|
1836
|
+
this._axisHelpersDirty = false;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
_updateAxisHelpers() {
|
|
1840
|
+
if (!this.camera || !this.scene) return;
|
|
1841
|
+
if (this._axisHelpersDirty) this._collectAxisHelpers();
|
|
1842
|
+
if (!this._axisHelpers || this._axisHelpers.size === 0) return;
|
|
1843
|
+
|
|
1844
|
+
const { width, height } = this._getContainerSize();
|
|
1845
|
+
const wpp = this._worldPerPixel(this.camera, width, height);
|
|
1846
|
+
if (!Number.isFinite(wpp) || wpp <= 0) return;
|
|
1847
|
+
|
|
1848
|
+
const parentScale = new THREE.Vector3(1, 1, 1);
|
|
1849
|
+
const eps = 1e-9;
|
|
1850
|
+
const setRes = (mat) => {
|
|
1851
|
+
if (mat?.resolution && typeof mat.resolution.set === 'function') {
|
|
1852
|
+
mat.resolution.set(width, height);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
for (const helper of this._axisHelpers) {
|
|
1857
|
+
if (!helper || !helper.isObject3D) continue;
|
|
1858
|
+
const px = Number(helper.userData?.axisHelperPx);
|
|
1859
|
+
const axisPx = Number.isFinite(px) ? px : (this._axisHelperPx || DEFAULT_AXIS_HELPER_PX);
|
|
1860
|
+
const axisLen = wpp * axisPx;
|
|
1861
|
+
|
|
1862
|
+
let sx = axisLen;
|
|
1863
|
+
let sy = axisLen;
|
|
1864
|
+
let sz = axisLen;
|
|
1865
|
+
const compensate = helper.userData?.axisHelperCompensateScale !== false;
|
|
1866
|
+
if (compensate && helper.parent && typeof helper.parent.getWorldScale === 'function') {
|
|
1867
|
+
try { helper.parent.updateMatrixWorld?.(true); } catch { }
|
|
1868
|
+
helper.parent.getWorldScale(parentScale);
|
|
1869
|
+
const safe = (v) => (Math.abs(v) < eps ? 1 : Math.abs(v));
|
|
1870
|
+
sx /= safe(parentScale.x);
|
|
1871
|
+
sy /= safe(parentScale.y);
|
|
1872
|
+
sz /= safe(parentScale.z);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const last = helper.userData._axisHelperScale;
|
|
1876
|
+
if (!last
|
|
1877
|
+
|| Math.abs(last.x - sx) > 1e-6
|
|
1878
|
+
|| Math.abs(last.y - sy) > 1e-6
|
|
1879
|
+
|| Math.abs(last.z - sz) > 1e-6) {
|
|
1880
|
+
helper.scale.set(sx, sy, sz);
|
|
1881
|
+
helper.userData._axisHelperScale = { x: sx, y: sy, z: sz };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
helper.traverse?.((node) => {
|
|
1885
|
+
const mat = node?.material;
|
|
1886
|
+
if (!mat) return;
|
|
1887
|
+
if (Array.isArray(mat)) mat.forEach(setRes);
|
|
1888
|
+
else setRes(mat);
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
_computeSceneBounds({ reuse = false, includeExcluded = false } = {}) {
|
|
1894
|
+
if (reuse && this._sceneBoundsCache) return this._sceneBoundsCache;
|
|
1895
|
+
const box = new THREE.Box3();
|
|
1896
|
+
const tmp = new THREE.Box3();
|
|
1897
|
+
let hasBounds = false;
|
|
1898
|
+
if (!this.scene) return null;
|
|
1899
|
+
try { this.scene.updateMatrixWorld(true); } catch { }
|
|
1900
|
+
|
|
1901
|
+
const shouldSkip = (obj) => {
|
|
1902
|
+
const ud = obj?.userData;
|
|
1903
|
+
if (ud?.axisHelper) return true;
|
|
1904
|
+
if (!includeExcluded && ud?.excludeFromFit) return true;
|
|
1905
|
+
return false;
|
|
1906
|
+
};
|
|
1907
|
+
const visit = (obj, skipParent) => {
|
|
1908
|
+
if (!obj) return;
|
|
1909
|
+
const skip = skipParent || shouldSkip(obj);
|
|
1910
|
+
if (!skip) {
|
|
1911
|
+
const geom = obj.geometry;
|
|
1912
|
+
if (geom) {
|
|
1913
|
+
let bbox = null;
|
|
1914
|
+
if (obj.boundingBox !== undefined) {
|
|
1915
|
+
if (obj.boundingBox == null && typeof obj.computeBoundingBox === 'function') {
|
|
1916
|
+
try { obj.computeBoundingBox(); } catch { }
|
|
1917
|
+
}
|
|
1918
|
+
bbox = obj.boundingBox;
|
|
1919
|
+
} else {
|
|
1920
|
+
if (geom.boundingBox == null && typeof geom.computeBoundingBox === 'function') {
|
|
1921
|
+
try { geom.computeBoundingBox(); } catch { }
|
|
1922
|
+
}
|
|
1923
|
+
bbox = geom.boundingBox;
|
|
1924
|
+
}
|
|
1925
|
+
if (bbox) {
|
|
1926
|
+
tmp.copy(bbox);
|
|
1927
|
+
tmp.applyMatrix4(obj.matrixWorld);
|
|
1928
|
+
box.union(tmp);
|
|
1929
|
+
hasBounds = true;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const children = obj.children || [];
|
|
1934
|
+
for (const child of children) visit(child, skip);
|
|
1935
|
+
};
|
|
1936
|
+
visit(this.scene, false);
|
|
1937
|
+
|
|
1938
|
+
if (!hasBounds || box.isEmpty()) return null;
|
|
1939
|
+
this._sceneBoundsCache = box;
|
|
1940
|
+
return box;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
_updateDepthRange({ reuseBounds = false } = {}) {
|
|
1944
|
+
if (!this.camera) return false;
|
|
1945
|
+
const box = this._computeSceneBounds({ reuse: reuseBounds, includeExcluded: true });
|
|
1946
|
+
if (!box) return false;
|
|
1947
|
+
try { this.camera.updateMatrixWorld(true); } catch { /* ignore */ }
|
|
1948
|
+
|
|
1949
|
+
const corners = [
|
|
1950
|
+
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
|
|
1951
|
+
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
|
|
1952
|
+
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
|
|
1953
|
+
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
|
|
1954
|
+
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
|
|
1955
|
+
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
|
|
1956
|
+
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
|
|
1957
|
+
new THREE.Vector3(box.max.x, box.max.y, box.max.z),
|
|
1958
|
+
];
|
|
1959
|
+
const inv = new THREE.Matrix4().copy(this.camera.matrixWorld).invert();
|
|
1960
|
+
let minZ = Infinity;
|
|
1961
|
+
let maxZ = -Infinity;
|
|
1962
|
+
for (const p of corners) {
|
|
1963
|
+
p.applyMatrix4(inv);
|
|
1964
|
+
if (p.z < minZ) minZ = p.z;
|
|
1965
|
+
if (p.z > maxZ) maxZ = p.z;
|
|
1966
|
+
}
|
|
1967
|
+
if (!Number.isFinite(minZ) || !Number.isFinite(maxZ)) return false;
|
|
1968
|
+
|
|
1969
|
+
const range = Math.max(1e-6, maxZ - minZ);
|
|
1970
|
+
const diag = box.min.distanceTo(box.max);
|
|
1971
|
+
const pad = Math.max(range * 0.1, diag * 0.1, 0.5);
|
|
1972
|
+
if (maxZ > (-pad + 1e-6)) {
|
|
1973
|
+
const dir = new THREE.Vector3();
|
|
1974
|
+
try { this.camera.getWorldDirection(dir); } catch { dir.set(0, 0, -1); }
|
|
1975
|
+
if (dir.lengthSq() > 0) {
|
|
1976
|
+
const shift = maxZ + pad;
|
|
1977
|
+
dir.normalize();
|
|
1978
|
+
this.camera.position.addScaledVector(dir, -shift);
|
|
1979
|
+
minZ -= shift;
|
|
1980
|
+
maxZ -= shift;
|
|
1981
|
+
try { this.camera.updateMatrixWorld(true); } catch { /* ignore */ }
|
|
1982
|
+
try { this.controls?.updateMatrixState?.(); } catch { /* ignore */ }
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const near = 0;
|
|
1987
|
+
let far = Math.max(1, -minZ + pad);
|
|
1988
|
+
if (!Number.isFinite(far)) return false;
|
|
1989
|
+
|
|
1990
|
+
const nearChanged = Math.abs((this.camera.near || 0) - near) > 1e-6;
|
|
1991
|
+
const farChanged = Math.abs((this.camera.far || 0) - far) > 1e-6;
|
|
1992
|
+
if (nearChanged || farChanged) {
|
|
1993
|
+
this.camera.near = near;
|
|
1994
|
+
this.camera.far = far;
|
|
1995
|
+
try { this.camera.updateProjectionMatrix(); } catch { /* ignore */ }
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// Zoom-to-fit using only ArcballControls operations (pan + zoom).
|
|
2001
|
+
// Does not alter camera orientation or frustum parameters (left/right/top/bottom).
|
|
2002
|
+
zoomToFit(margin = 1.1) {
|
|
2003
|
+
try {
|
|
2004
|
+
const c = this.controls;
|
|
2005
|
+
if (!c) return;
|
|
2006
|
+
|
|
2007
|
+
const box = this._computeSceneBounds();
|
|
2008
|
+
if (!box) return;
|
|
2009
|
+
|
|
2010
|
+
// Ensure matrices are current
|
|
2011
|
+
this.camera.updateMatrixWorld(true);
|
|
2012
|
+
|
|
2013
|
+
// Compute extents in camera space (preserve orientation)
|
|
2014
|
+
const corners = [
|
|
2015
|
+
new THREE.Vector3(box.min.x, box.min.y, box.min.z),
|
|
2016
|
+
new THREE.Vector3(box.min.x, box.min.y, box.max.z),
|
|
2017
|
+
new THREE.Vector3(box.min.x, box.max.y, box.min.z),
|
|
2018
|
+
new THREE.Vector3(box.min.x, box.max.y, box.max.z),
|
|
2019
|
+
new THREE.Vector3(box.max.x, box.min.y, box.min.z),
|
|
2020
|
+
new THREE.Vector3(box.max.x, box.min.y, box.max.z),
|
|
2021
|
+
new THREE.Vector3(box.max.x, box.max.y, box.min.z),
|
|
2022
|
+
new THREE.Vector3(box.max.x, box.max.y, box.max.z),
|
|
2023
|
+
];
|
|
2024
|
+
const inv = new THREE.Matrix4().copy(this.camera.matrixWorld).invert();
|
|
2025
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2026
|
+
for (const p of corners) {
|
|
2027
|
+
p.applyMatrix4(inv);
|
|
2028
|
+
if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
|
|
2029
|
+
if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
|
|
2030
|
+
}
|
|
2031
|
+
const camWidth = Math.max(1e-6, (maxX - minX));
|
|
2032
|
+
const camHeight = Math.max(1e-6, (maxY - minY));
|
|
2033
|
+
|
|
2034
|
+
// Compute target zoom for orthographic camera using current frustum and viewport aspect.
|
|
2035
|
+
const { width, height } = this._getContainerSize();
|
|
2036
|
+
const aspect = Math.max(1e-6, width / height);
|
|
2037
|
+
const v = this.viewSize; // current half-height before zoom scaling
|
|
2038
|
+
const halfW = camWidth / 2 * Math.max(1, margin);
|
|
2039
|
+
const halfH = camHeight / 2 * Math.max(1, margin);
|
|
2040
|
+
const maxZoomByHeight = v / halfH;
|
|
2041
|
+
const maxZoomByWidth = (v * aspect) / halfW;
|
|
2042
|
+
const targetZoom = Math.min(maxZoomByHeight, maxZoomByWidth);
|
|
2043
|
+
const currentZoom = this.camera.zoom || 1;
|
|
2044
|
+
const sizeFactor = Math.max(1e-6, targetZoom / currentZoom);
|
|
2045
|
+
|
|
2046
|
+
// Compute world center of the box
|
|
2047
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
2048
|
+
|
|
2049
|
+
// Perform pan+zoom via ArcballControls only
|
|
2050
|
+
try { c.updateMatrixState && c.updateMatrixState(); } catch { }
|
|
2051
|
+
c.focus(center, sizeFactor);
|
|
2052
|
+
|
|
2053
|
+
// Sync and render
|
|
2054
|
+
try { c.update && c.update(); } catch { }
|
|
2055
|
+
this.render();
|
|
2056
|
+
} catch { /* noop */ }
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Wireframe toggle for all materials
|
|
2060
|
+
setWireframe(enabled) {
|
|
2061
|
+
this._wireframeEnabled = !!enabled;
|
|
2062
|
+
try {
|
|
2063
|
+
this.scene.traverse((obj) => {
|
|
2064
|
+
if (!obj) return;
|
|
2065
|
+
// Exclude transform gizmo hierarchy from wireframe toggling
|
|
2066
|
+
try {
|
|
2067
|
+
let p = obj;
|
|
2068
|
+
while (p) {
|
|
2069
|
+
if (p.isTransformGizmo) return;
|
|
2070
|
+
p = p.parent;
|
|
2071
|
+
}
|
|
2072
|
+
} catch { }
|
|
2073
|
+
// Exclude edge/loop/line objects from wireframe toggling
|
|
2074
|
+
if (obj.type === 'EDGE' || obj.type === 'LOOP' || obj.isLine || obj.isLine2 || obj.isLineSegments || obj.isLineLoop) return;
|
|
2075
|
+
|
|
2076
|
+
const apply = (mat) => { if (mat && 'wireframe' in mat) mat.wireframe = !!enabled; };
|
|
2077
|
+
if (obj.material) {
|
|
2078
|
+
if (Array.isArray(obj.material)) obj.material.forEach(apply); else apply(obj.material);
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
} catch { /* ignore */ }
|
|
2082
|
+
this.render();
|
|
2083
|
+
}
|
|
2084
|
+
toggleWireframe() { this.setWireframe(!this._wireframeEnabled); }
|
|
2085
|
+
|
|
2086
|
+
applyMetadataColors(target = null) {
|
|
2087
|
+
const metadataManager = this.partHistory?.metadataManager;
|
|
2088
|
+
const scene = this.partHistory?.scene || this.scene;
|
|
2089
|
+
if (!metadataManager || !scene) return;
|
|
2090
|
+
|
|
2091
|
+
const size = this.renderer?.getSize?.(new THREE.Vector2()) || null;
|
|
2092
|
+
const width = Math.max(1, size?.width || this.renderer?.domElement?.clientWidth || 1);
|
|
2093
|
+
const height = Math.max(1, size?.height || this.renderer?.domElement?.clientHeight || 1);
|
|
2094
|
+
|
|
2095
|
+
const solidKeys = ['solidColor', 'color'];
|
|
2096
|
+
const faceKeys = ['faceColor', 'color'];
|
|
2097
|
+
const edgeKeys = ['edgeColor', 'color'];
|
|
2098
|
+
const solidEdgeKeys = ['edgeColor'];
|
|
2099
|
+
|
|
2100
|
+
const pickColorValue = (meta, keys) => {
|
|
2101
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
2102
|
+
for (const key of keys) {
|
|
2103
|
+
if (!Object.prototype.hasOwnProperty.call(meta, key)) continue;
|
|
2104
|
+
const raw = meta[key];
|
|
2105
|
+
if (raw == null) continue;
|
|
2106
|
+
if (typeof raw === 'string' && raw.trim() === '') continue;
|
|
2107
|
+
return raw;
|
|
2108
|
+
}
|
|
2109
|
+
return null;
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
const parseColor = (raw) => {
|
|
2113
|
+
if (raw == null) return null;
|
|
2114
|
+
if (raw?.isColor) {
|
|
2115
|
+
try { return typeof raw.clone === 'function' ? raw.clone() : raw; } catch { return raw; }
|
|
2116
|
+
}
|
|
2117
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
2118
|
+
try { return new THREE.Color(raw); } catch { return null; }
|
|
2119
|
+
}
|
|
2120
|
+
if (typeof raw === 'string') {
|
|
2121
|
+
const v = raw.trim();
|
|
2122
|
+
if (!v) return null;
|
|
2123
|
+
const lower = v.toLowerCase();
|
|
2124
|
+
const isHex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(lower);
|
|
2125
|
+
const isHex0x = /^0x[0-9a-f]{6}$/.test(lower);
|
|
2126
|
+
const isFunc = /^(rgb|rgba|hsl|hsla)\(/.test(lower);
|
|
2127
|
+
if (!isHex && !isHex0x && !isFunc) return null;
|
|
2128
|
+
if (isHex0x) {
|
|
2129
|
+
const num = Number(v);
|
|
2130
|
+
if (Number.isFinite(num)) {
|
|
2131
|
+
try { return new THREE.Color(num); } catch { return null; }
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
try { return new THREE.Color(v); } catch { return null; }
|
|
2135
|
+
}
|
|
2136
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
2137
|
+
const r = Number(raw[0]);
|
|
2138
|
+
const g = Number(raw[1]);
|
|
2139
|
+
const b = Number(raw[2]);
|
|
2140
|
+
if (![r, g, b].every(Number.isFinite)) return null;
|
|
2141
|
+
const max = Math.max(r, g, b);
|
|
2142
|
+
try {
|
|
2143
|
+
if (max > 1) return new THREE.Color(r / 255, g / 255, b / 255);
|
|
2144
|
+
return new THREE.Color(r, g, b);
|
|
2145
|
+
} catch { return null; }
|
|
2146
|
+
}
|
|
2147
|
+
if (typeof raw === 'object') {
|
|
2148
|
+
const r = Number(raw.r);
|
|
2149
|
+
const g = Number(raw.g);
|
|
2150
|
+
const b = Number(raw.b);
|
|
2151
|
+
if ([r, g, b].every(Number.isFinite)) {
|
|
2152
|
+
const max = Math.max(r, g, b);
|
|
2153
|
+
try {
|
|
2154
|
+
if (max > 1) return new THREE.Color(r / 255, g / 255, b / 255);
|
|
2155
|
+
return new THREE.Color(r, g, b);
|
|
2156
|
+
} catch { return null; }
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return null;
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
const getMeta = (name) => {
|
|
2163
|
+
if (!name || typeof metadataManager.getMetadata !== 'function') return null;
|
|
2164
|
+
try { return metadataManager.getMetadata(name); } catch { return null; }
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
const applyMaterial = (obj, baseMaterial, color) => {
|
|
2168
|
+
if (!obj || !baseMaterial) return;
|
|
2169
|
+
if (!obj.userData) obj.userData = {};
|
|
2170
|
+
const ud = obj.userData;
|
|
2171
|
+
const defaultMaterial = ud.__defaultMaterial ?? baseMaterial;
|
|
2172
|
+
if (!ud.__defaultMaterial) ud.__defaultMaterial = baseMaterial;
|
|
2173
|
+
const isHovered = !!ud.__hoverMatApplied;
|
|
2174
|
+
const isSelected = obj.selected === true;
|
|
2175
|
+
|
|
2176
|
+
const applyBase = (mat) => {
|
|
2177
|
+
ud.__baseMaterial = mat;
|
|
2178
|
+
if (isHovered) {
|
|
2179
|
+
ud.__hoverOrigMat = mat;
|
|
2180
|
+
} else if (!isSelected && mat) {
|
|
2181
|
+
obj.material = mat;
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
if (!color) {
|
|
2186
|
+
if (ud.__metadataMaterial && ud.__metadataMaterial !== defaultMaterial) {
|
|
2187
|
+
try { ud.__metadataMaterial.dispose?.(); } catch { }
|
|
2188
|
+
}
|
|
2189
|
+
try { delete ud.__metadataMaterial; } catch { }
|
|
2190
|
+
try { delete ud.__metadataColor; } catch { }
|
|
2191
|
+
applyBase(defaultMaterial);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const colorHex = color.getHexString();
|
|
2196
|
+
if (ud.__metadataColor === colorHex && ud.__metadataMaterial) {
|
|
2197
|
+
applyBase(ud.__metadataMaterial);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
let nextMat = null;
|
|
2202
|
+
try { nextMat = typeof baseMaterial.clone === 'function' ? baseMaterial.clone() : null; } catch { nextMat = null; }
|
|
2203
|
+
if (!nextMat) return;
|
|
2204
|
+
try {
|
|
2205
|
+
if (nextMat.color && typeof nextMat.color.set === 'function') nextMat.color.set(color);
|
|
2206
|
+
} catch { }
|
|
2207
|
+
try {
|
|
2208
|
+
if (nextMat.resolution && typeof nextMat.resolution.set === 'function') {
|
|
2209
|
+
nextMat.resolution.set(width, height);
|
|
2210
|
+
}
|
|
2211
|
+
} catch { }
|
|
2212
|
+
try { nextMat.needsUpdate = true; } catch { }
|
|
2213
|
+
|
|
2214
|
+
if (ud.__metadataMaterial && ud.__metadataMaterial !== defaultMaterial) {
|
|
2215
|
+
try { ud.__metadataMaterial.dispose?.(); } catch { }
|
|
2216
|
+
}
|
|
2217
|
+
ud.__metadataColor = colorHex;
|
|
2218
|
+
ud.__metadataMaterial = nextMat;
|
|
2219
|
+
applyBase(nextMat);
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
const applyToSolid = (solid) => {
|
|
2223
|
+
if (!solid || solid.type !== 'SOLID') return;
|
|
2224
|
+
const solidMeta = getMeta(solid.name);
|
|
2225
|
+
const solidUserMeta = solid?.userData?.metadata || null;
|
|
2226
|
+
const solidColor = parseColor(
|
|
2227
|
+
pickColorValue(solidMeta, solidKeys)
|
|
2228
|
+
?? pickColorValue(solidUserMeta, solidKeys)
|
|
2229
|
+
);
|
|
2230
|
+
const solidEdgeColor = parseColor(
|
|
2231
|
+
pickColorValue(solidMeta, solidEdgeKeys)
|
|
2232
|
+
?? pickColorValue(solidUserMeta, solidEdgeKeys)
|
|
2233
|
+
);
|
|
2234
|
+
const children = Array.isArray(solid.children) ? solid.children : [];
|
|
2235
|
+
|
|
2236
|
+
for (const child of children) {
|
|
2237
|
+
if (!child) continue;
|
|
2238
|
+
if (child.type === 'FACE') {
|
|
2239
|
+
const faceName = child.name || child.userData?.faceName || null;
|
|
2240
|
+
const managerMeta = faceName ? getMeta(faceName) : null;
|
|
2241
|
+
let faceMeta = null;
|
|
2242
|
+
if (faceName && typeof solid.getFaceMetadata === 'function') {
|
|
2243
|
+
try { faceMeta = solid.getFaceMetadata(faceName); } catch { faceMeta = null; }
|
|
2244
|
+
}
|
|
2245
|
+
const faceColor = parseColor(
|
|
2246
|
+
pickColorValue(managerMeta, faceKeys)
|
|
2247
|
+
?? pickColorValue(faceMeta, faceKeys)
|
|
2248
|
+
) || solidColor;
|
|
2249
|
+
const baseFace = CADmaterials.FACE?.BASE ?? child.material;
|
|
2250
|
+
applyMaterial(child, baseFace, faceColor);
|
|
2251
|
+
} else if (child.type === 'EDGE') {
|
|
2252
|
+
const edgeName = child.name || null;
|
|
2253
|
+
const managerMeta = edgeName ? getMeta(edgeName) : null;
|
|
2254
|
+
let edgeMeta = null;
|
|
2255
|
+
if (edgeName && typeof solid.getEdgeMetadata === 'function') {
|
|
2256
|
+
try { edgeMeta = solid.getEdgeMetadata(edgeName); } catch { edgeMeta = null; }
|
|
2257
|
+
}
|
|
2258
|
+
let edgeColor = parseColor(
|
|
2259
|
+
pickColorValue(managerMeta, edgeKeys)
|
|
2260
|
+
?? pickColorValue(edgeMeta, edgeKeys)
|
|
2261
|
+
);
|
|
2262
|
+
if (!edgeColor && solidEdgeColor) edgeColor = solidEdgeColor;
|
|
2263
|
+
|
|
2264
|
+
const isBoundary = !!(child.userData?.faceA || child.userData?.faceB);
|
|
2265
|
+
const baseEdge = isBoundary ? (CADmaterials.EDGE?.BASE ?? child.material)
|
|
2266
|
+
: (child.userData?.__defaultMaterial ?? child.material);
|
|
2267
|
+
applyMaterial(child, baseEdge, edgeColor);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
const resolveSolid = (obj) => {
|
|
2273
|
+
if (!obj) return null;
|
|
2274
|
+
if (obj.type === 'SOLID') return obj;
|
|
2275
|
+
if (obj.parentSolid) return obj.parentSolid;
|
|
2276
|
+
let current = obj.parent;
|
|
2277
|
+
while (current) {
|
|
2278
|
+
if (current.type === 'SOLID') return current;
|
|
2279
|
+
current = current.parent;
|
|
2280
|
+
}
|
|
2281
|
+
return null;
|
|
2282
|
+
};
|
|
2283
|
+
|
|
2284
|
+
if (target) {
|
|
2285
|
+
let obj = target;
|
|
2286
|
+
if (typeof obj === 'string') {
|
|
2287
|
+
try { obj = scene.getObjectByName(obj); } catch { obj = null; }
|
|
2288
|
+
}
|
|
2289
|
+
const solid = resolveSolid(obj);
|
|
2290
|
+
if (solid) {
|
|
2291
|
+
applyToSolid(solid);
|
|
2292
|
+
} else if (obj && (obj.type === 'FACE' || obj.type === 'EDGE')) {
|
|
2293
|
+
const name = obj.name || null;
|
|
2294
|
+
const managerMeta = name ? getMeta(name) : null;
|
|
2295
|
+
const keys = obj.type === 'FACE' ? faceKeys : edgeKeys;
|
|
2296
|
+
const color = parseColor(pickColorValue(managerMeta, keys));
|
|
2297
|
+
const baseMat = obj.type === 'FACE'
|
|
2298
|
+
? (CADmaterials.FACE?.BASE ?? obj.material)
|
|
2299
|
+
: (CADmaterials.EDGE?.BASE ?? obj.material);
|
|
2300
|
+
applyMaterial(obj, baseMat, color);
|
|
2301
|
+
}
|
|
2302
|
+
} else {
|
|
2303
|
+
scene.traverse((obj) => {
|
|
2304
|
+
if (obj && obj.type === 'SOLID') applyToSolid(obj);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
try { this.render(); } catch { }
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// ----------------------------------------
|
|
2312
|
+
// Internal: Animation Loop
|
|
2313
|
+
// ----------------------------------------
|
|
2314
|
+
_loop() {
|
|
2315
|
+
this._raf = requestAnimationFrame(this._loop);
|
|
2316
|
+
this.controls.update();
|
|
2317
|
+
try {
|
|
2318
|
+
const ax = (typeof window !== 'undefined') ? (window.__BREP_activeXform || null) : null;
|
|
2319
|
+
const tc = ax && ax.controls;
|
|
2320
|
+
if (tc) {
|
|
2321
|
+
if (typeof tc.update === 'function') tc.update();
|
|
2322
|
+
else tc.updateMatrixWorld(true);
|
|
2323
|
+
}
|
|
2324
|
+
} catch { }
|
|
2325
|
+
this.render();
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// ----------------------------------------
|
|
2329
|
+
// Internal: Picking helpers
|
|
2330
|
+
// ----------------------------------------
|
|
2331
|
+
_getPointerNDC(event) {
|
|
2332
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
2333
|
+
const x = (event.clientX - rect.left) / rect.width;
|
|
2334
|
+
const y = (event.clientY - rect.top) / rect.height;
|
|
2335
|
+
// Convert to NDC (-1..1)
|
|
2336
|
+
return new THREE.Vector2(x * 2 - 1, -(y * 2 - 1));
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
_isEventOverRenderer(event) {
|
|
2340
|
+
if (!event || !this.renderer?.domElement) return false;
|
|
2341
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
2342
|
+
const x = event.clientX;
|
|
2343
|
+
const y = event.clientY;
|
|
2344
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
_mapIntersectionToTarget(intersection, options = {}) {
|
|
2348
|
+
if (!intersection || !intersection.object) return null;
|
|
2349
|
+
const { allowAnyAllowedType = false, ignoreSelectionFilter = false } = options;
|
|
2350
|
+
const isAllowed = (type) => {
|
|
2351
|
+
if (!type) return false;
|
|
2352
|
+
if (ignoreSelectionFilter) return true;
|
|
2353
|
+
if (allowAnyAllowedType && typeof SelectionFilter.matchesAllowedType === 'function') {
|
|
2354
|
+
return SelectionFilter.matchesAllowedType(type);
|
|
2355
|
+
}
|
|
2356
|
+
if (typeof SelectionFilter.IsAllowed === 'function') {
|
|
2357
|
+
return SelectionFilter.IsAllowed(type);
|
|
2358
|
+
}
|
|
2359
|
+
return true;
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
// Prefer the intersected object if it is clickable
|
|
2363
|
+
let obj = intersection.object;
|
|
2364
|
+
|
|
2365
|
+
// If the object (or its ancestors) doesn't expose onClick, climb to one that does
|
|
2366
|
+
let target = obj;
|
|
2367
|
+
while (target && typeof target.onClick !== 'function' && target.visible) target = target.parent;
|
|
2368
|
+
if (!target) return null;
|
|
2369
|
+
|
|
2370
|
+
// Respect selection filter: ensure target is a permitted type, or ALL
|
|
2371
|
+
if (typeof isAllowed === 'function') {
|
|
2372
|
+
// Allow selecting already-selected items regardless (toggle off), consistent with SceneListing
|
|
2373
|
+
if (!isAllowed(target.type) && !target.selected) {
|
|
2374
|
+
// Try to find a closer ancestor/descendant of allowed type that is clickable
|
|
2375
|
+
// Ascend first (e.g., FACE hit while EDGE is active should try parent SOLID only if allowed)
|
|
2376
|
+
let t = target.parent;
|
|
2377
|
+
while (t && typeof t.onClick === 'function' && !isAllowed(t.type)) t = t.parent;
|
|
2378
|
+
if (t && typeof t.onClick === 'function' && isAllowed(t.type)) target = t;
|
|
2379
|
+
else return null;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return target;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
_pickAtEvent(event, options = {}) {
|
|
2386
|
+
const { collectAll = false, allowAnyAllowedType = false, ignoreSelectionFilter = false } = options;
|
|
2387
|
+
// While Sketch Mode is active, suppress normal scene picking
|
|
2388
|
+
// SketchMode3D manages its own picking for sketch points/curves and model edges.
|
|
2389
|
+
if (this._sketchMode) return collectAll ? { hit: null, target: null, candidates: [] } : { hit: null, target: null };
|
|
2390
|
+
|
|
2391
|
+
// Auto-clear stale spline mode so normal picking resumes after leaving the spline dialog
|
|
2392
|
+
if (this._splineMode) {
|
|
2393
|
+
try {
|
|
2394
|
+
const validSession = typeof this._splineMode.isActive === 'function';
|
|
2395
|
+
const stillActive = validSession ? this._splineMode.isActive() : false;
|
|
2396
|
+
if (!validSession || !stillActive) {
|
|
2397
|
+
this.endSplineMode();
|
|
2398
|
+
}
|
|
2399
|
+
} catch {
|
|
2400
|
+
this.endSplineMode();
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// In spline mode, allow picking only spline vertices, suppress other scene picking
|
|
2405
|
+
if (this._splineMode) {
|
|
2406
|
+
if (!event) return collectAll ? { hit: null, target: null, candidates: [] } : { hit: null, target: null };
|
|
2407
|
+
const ndc = this._getPointerNDC(event);
|
|
2408
|
+
this.raycaster.setFromCamera(ndc, this.camera);
|
|
2409
|
+
// Set up raycaster params for vertex picking
|
|
2410
|
+
try {
|
|
2411
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
2412
|
+
const wpp = this._worldPerPixel(this.camera, rect.width, rect.height);
|
|
2413
|
+
this.raycaster.params.Points = this.raycaster.params.Points || {};
|
|
2414
|
+
this.raycaster.params.Points.threshold = Math.max(0.05, wpp * 6);
|
|
2415
|
+
} catch { }
|
|
2416
|
+
|
|
2417
|
+
// Only intersect spline vertices
|
|
2418
|
+
const intersects = this._withDoubleSidedPicking(() => this.raycaster.intersectObjects(this.scene.children, true));
|
|
2419
|
+
|
|
2420
|
+
for (const it of intersects) {
|
|
2421
|
+
if (!it || !it.object) continue;
|
|
2422
|
+
|
|
2423
|
+
// Check if this is a spline vertex by looking at userData
|
|
2424
|
+
if (it.object.userData?.isSplineVertex || it.object.userData?.isSplineWeight) {
|
|
2425
|
+
const target = it.object;
|
|
2426
|
+
if (typeof target.onClick === 'function') {
|
|
2427
|
+
return { hit: it, target };
|
|
2428
|
+
} else {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return collectAll ? { hit: null, target: null, candidates: [] } : { hit: null, target: null };
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (!event) return collectAll ? { hit: null, target: null, candidates: [] } : { hit: null, target: null };
|
|
2436
|
+
const ndc = this._getPointerNDC(event);
|
|
2437
|
+
try { this.camera.updateMatrixWorld(true); } catch { /* ignore */ }
|
|
2438
|
+
this.raycaster.setFromCamera(ndc, this.camera);
|
|
2439
|
+
// Tune line picking thresholds per-frame based on zoom and DPI
|
|
2440
|
+
try {
|
|
2441
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
2442
|
+
const wpp = this._worldPerPixel(this.camera, rect.width, rect.height);
|
|
2443
|
+
this.raycaster.params.Line = this.raycaster.params.Line || {};
|
|
2444
|
+
this.raycaster.params.Line.threshold = Math.max(0.05, wpp * 6);
|
|
2445
|
+
const dpr = (window.devicePixelRatio || 1);
|
|
2446
|
+
this.raycaster.params.Line2 = this.raycaster.params.Line2 || {};
|
|
2447
|
+
this.raycaster.params.Line2.threshold = Math.max(1, 2 * dpr);
|
|
2448
|
+
// Improve point picking tolerance using world-units per pixel
|
|
2449
|
+
this.raycaster.params.Points = this.raycaster.params.Points || {};
|
|
2450
|
+
this.raycaster.params.Points.threshold = Math.max(0.05, wpp * 6);
|
|
2451
|
+
} catch { }
|
|
2452
|
+
// Fix ray origin - ensure it starts from behind the camera for large scenes
|
|
2453
|
+
try {
|
|
2454
|
+
const ray = this.raycaster.ray;
|
|
2455
|
+
const dir = ray.direction.clone().normalize();
|
|
2456
|
+
const span = Math.max(
|
|
2457
|
+
1,
|
|
2458
|
+
Math.abs(this.camera.far || 0),
|
|
2459
|
+
Math.abs(this.camera.near || 0),
|
|
2460
|
+
this.viewSize * 40
|
|
2461
|
+
);
|
|
2462
|
+
ray.origin.addScaledVector(dir, -span);
|
|
2463
|
+
} catch { }
|
|
2464
|
+
// Intersect everything; raycaster will skip non-geometry nodes
|
|
2465
|
+
const intersects = this._withDoubleSidedPicking(() => this.raycaster.intersectObjects(this.scene.children, true));
|
|
2466
|
+
|
|
2467
|
+
// DEBUG: Log all objects under mouse pointer in normal mode
|
|
2468
|
+
if (intersects.length > 0) {
|
|
2469
|
+
debugLog(`NORMAL MODE CLICK DEBUG:`);
|
|
2470
|
+
debugLog(`- Mouse NDC: (${ndc.x.toFixed(3)}, ${ndc.y.toFixed(3)})`);
|
|
2471
|
+
debugLog(`- Total intersections found: ${intersects.length}`);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const candidates = [];
|
|
2475
|
+
for (const it of intersects) {
|
|
2476
|
+
// skip entities that are not visible (or have invisible parents)
|
|
2477
|
+
if (!it || !it.object) continue;
|
|
2478
|
+
const testVisible = (obj) => {
|
|
2479
|
+
if (obj.parent === null) {
|
|
2480
|
+
return true;
|
|
2481
|
+
}
|
|
2482
|
+
if (obj.visible === false) return false;
|
|
2483
|
+
return testVisible(obj.parent);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const visibleResult = testVisible(it.object);
|
|
2487
|
+
|
|
2488
|
+
if (visibleResult) {
|
|
2489
|
+
|
|
2490
|
+
const target = this._mapIntersectionToTarget(it, { allowAnyAllowedType, ignoreSelectionFilter });
|
|
2491
|
+
if (target) {
|
|
2492
|
+
if (collectAll) {
|
|
2493
|
+
candidates.push({ hit: it, target, distance: it.distance ?? Infinity });
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
return { hit: it, target };
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
}
|
|
2503
|
+
if (collectAll) {
|
|
2504
|
+
return {
|
|
2505
|
+
hit: candidates[0]?.hit || null,
|
|
2506
|
+
target: candidates[0]?.target || null,
|
|
2507
|
+
candidates,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
return { hit: null, target: null };
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Temporarily make FrontSide materials DoubleSide for picking without changing render appearance.
|
|
2514
|
+
_withDoubleSidedPicking(fn) {
|
|
2515
|
+
if (!fn) return null;
|
|
2516
|
+
const touched = new Set();
|
|
2517
|
+
const markMaterial = (mat) => {
|
|
2518
|
+
if (!mat || typeof mat.side === 'undefined') return;
|
|
2519
|
+
if (mat.side === THREE.FrontSide) {
|
|
2520
|
+
touched.add(mat);
|
|
2521
|
+
mat.side = THREE.DoubleSide;
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
try {
|
|
2525
|
+
if (this.scene && typeof this.scene.traverse === 'function') {
|
|
2526
|
+
this.scene.traverse((obj) => {
|
|
2527
|
+
if (!obj) return;
|
|
2528
|
+
const m = obj.material;
|
|
2529
|
+
if (Array.isArray(m)) m.forEach(markMaterial); else markMaterial(m);
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
return fn();
|
|
2533
|
+
} finally {
|
|
2534
|
+
for (const mat of touched) {
|
|
2535
|
+
try { mat.side = THREE.FrontSide; } catch { /* ignore */ }
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
_updateHover(event) {
|
|
2541
|
+
const { primary } = this._collectSelectionCandidates(event);
|
|
2542
|
+
if (primary) {
|
|
2543
|
+
try { SelectionFilter.setHoverObject(primary); } catch { }
|
|
2544
|
+
} else {
|
|
2545
|
+
try { SelectionFilter.clearHover(); } catch { }
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
_collectSelectionCandidates(event) {
|
|
2550
|
+
const allowedTypes = (() => {
|
|
2551
|
+
try {
|
|
2552
|
+
const list = SelectionFilter.getAvailableTypes?.() || [];
|
|
2553
|
+
if (Array.isArray(list) && list.length > 0) return list;
|
|
2554
|
+
if (Array.isArray(SelectionFilter.TYPES)) return SelectionFilter.TYPES.filter(t => t !== SelectionFilter.ALL);
|
|
2555
|
+
} catch { }
|
|
2556
|
+
return [];
|
|
2557
|
+
})();
|
|
2558
|
+
const normType = (t) => String(t || '').toUpperCase();
|
|
2559
|
+
const allowedSet = new Set(allowedTypes.map(normType));
|
|
2560
|
+
const priorityOrder = [
|
|
2561
|
+
SelectionFilter.VERTEX,
|
|
2562
|
+
SelectionFilter.EDGE,
|
|
2563
|
+
SelectionFilter.FACE,
|
|
2564
|
+
SelectionFilter.PLANE,
|
|
2565
|
+
SelectionFilter.SOLID,
|
|
2566
|
+
SelectionFilter.COMPONENT,
|
|
2567
|
+
].map(t => normType(t));
|
|
2568
|
+
const getPriority = (type) => {
|
|
2569
|
+
const idx = priorityOrder.indexOf(normType(type));
|
|
2570
|
+
return idx === -1 ? priorityOrder.length : idx;
|
|
2571
|
+
};
|
|
2572
|
+
const isAllowedType = (type) => {
|
|
2573
|
+
if (allowedSet.size === 0) return true;
|
|
2574
|
+
return allowedSet.has(normType(type));
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2577
|
+
const { target, candidates = [] } = this._pickAtEvent(event, { collectAll: true, allowAnyAllowedType: true });
|
|
2578
|
+
const deduped = [];
|
|
2579
|
+
const seen = new Set();
|
|
2580
|
+
const normalizeTarget = (obj) => {
|
|
2581
|
+
if (!obj) return null;
|
|
2582
|
+
let o = obj;
|
|
2583
|
+
const nt = normType(o.type);
|
|
2584
|
+
if (nt === 'POINTS' && o.parent && normType(o.parent.type) === normType(SelectionFilter.VERTEX)) {
|
|
2585
|
+
o = o.parent;
|
|
2586
|
+
}
|
|
2587
|
+
if (!isAllowedType(o.type) && o.parent && isAllowedType(o.parent.type)) {
|
|
2588
|
+
o = o.parent;
|
|
2589
|
+
}
|
|
2590
|
+
return o;
|
|
2591
|
+
};
|
|
2592
|
+
const addEntry = (obj, distance) => {
|
|
2593
|
+
const normalized = normalizeTarget(obj);
|
|
2594
|
+
if (!normalized) return;
|
|
2595
|
+
if (!isAllowedType(normalized.type)) return;
|
|
2596
|
+
const key = normalized.uuid || normalized.name || `${normalized.type}-${seen.size}`;
|
|
2597
|
+
if (seen.has(key)) return;
|
|
2598
|
+
seen.add(key);
|
|
2599
|
+
deduped.push({
|
|
2600
|
+
target: normalized,
|
|
2601
|
+
distance: Number.isFinite(distance) ? distance : Infinity,
|
|
2602
|
+
label: this._describeSelectionCandidate(normalized),
|
|
2603
|
+
});
|
|
2604
|
+
};
|
|
2605
|
+
for (const entry of candidates) {
|
|
2606
|
+
const obj = entry?.target;
|
|
2607
|
+
if (!obj) continue;
|
|
2608
|
+
const distance = Number.isFinite(entry?.distance) ? entry.distance : (entry?.hit?.distance ?? Infinity);
|
|
2609
|
+
addEntry(obj, distance);
|
|
2610
|
+
}
|
|
2611
|
+
deduped.sort((a, b) => a.distance - b.distance);
|
|
2612
|
+
|
|
2613
|
+
// When all types are allowed, also include ancestor SOLID/COMPONENT entries at the end
|
|
2614
|
+
const extras = [];
|
|
2615
|
+
const addExtra = (obj, distance) => {
|
|
2616
|
+
const normalized = normalizeTarget(obj);
|
|
2617
|
+
if (!normalized) return;
|
|
2618
|
+
if (!isAllowedType(normalized.type)) return;
|
|
2619
|
+
const key = normalized.uuid || normalized.name || `${normalized.type}-${seen.size}`;
|
|
2620
|
+
if (seen.has(key)) return;
|
|
2621
|
+
seen.add(key);
|
|
2622
|
+
extras.push({
|
|
2623
|
+
target: normalized,
|
|
2624
|
+
distance: Number.isFinite(distance) ? distance : Infinity,
|
|
2625
|
+
label: this._describeSelectionCandidate(normalized),
|
|
2626
|
+
});
|
|
2627
|
+
};
|
|
2628
|
+
const findAncestorOfType = (obj, type) => {
|
|
2629
|
+
let cur = obj?.parent || null;
|
|
2630
|
+
while (cur) {
|
|
2631
|
+
if (normType(cur.type) === normType(type)) return cur;
|
|
2632
|
+
cur = cur.parent || null;
|
|
2633
|
+
}
|
|
2634
|
+
return null;
|
|
2635
|
+
};
|
|
2636
|
+
for (const entry of deduped.slice()) {
|
|
2637
|
+
const obj = entry.target;
|
|
2638
|
+
const dist = entry.distance;
|
|
2639
|
+
const solid = findAncestorOfType(obj, SelectionFilter.SOLID);
|
|
2640
|
+
const component = findAncestorOfType(obj, SelectionFilter.COMPONENT);
|
|
2641
|
+
addExtra(component, dist);
|
|
2642
|
+
addExtra(solid, dist);
|
|
2643
|
+
}
|
|
2644
|
+
extras.sort((a, b) => a.distance - b.distance);
|
|
2645
|
+
const ordered = deduped.concat(extras);
|
|
2646
|
+
ordered.sort((a, b) => {
|
|
2647
|
+
const pa = getPriority(a?.target?.type);
|
|
2648
|
+
const pb = getPriority(b?.target?.type);
|
|
2649
|
+
if (pa !== pb) return pa - pb;
|
|
2650
|
+
return (a?.distance ?? Infinity) - (b?.distance ?? Infinity);
|
|
2651
|
+
});
|
|
2652
|
+
const primary = ordered[0]?.target || target || null;
|
|
2653
|
+
return { ordered, primary };
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
_selectAt(event) {
|
|
2657
|
+
const { ordered, primary } = this._collectSelectionCandidates(event);
|
|
2658
|
+
if (!primary) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (ordered.length > 1) {
|
|
2663
|
+
this._scheduleSelectionOverlay(event, ordered);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
this._hideSelectionOverlay();
|
|
2668
|
+
this._applySelectionTarget(primary);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
_applySelectionTarget(target, options = {}) {
|
|
2672
|
+
if (!target) return;
|
|
2673
|
+
this._lastInspectorTarget = target;
|
|
2674
|
+
this._lastInspectorSolid = this._findParentSolid(target);
|
|
2675
|
+
if (this._triangleDebugger && this._triangleDebugger.isOpen && this._triangleDebugger.isOpen()) {
|
|
2676
|
+
try { this._triangleDebugger.refreshTarget(target); } catch { }
|
|
2677
|
+
}
|
|
2678
|
+
const {
|
|
2679
|
+
triggerOnClick = true,
|
|
2680
|
+
allowDiagnostics = true,
|
|
2681
|
+
} = options;
|
|
2682
|
+
// One-shot diagnostic inspector
|
|
2683
|
+
if (allowDiagnostics && this._diagPickOnce) {
|
|
2684
|
+
this._diagPickOnce = false;
|
|
2685
|
+
try { this._showDiagnosticsFor(target); } catch (e) { try { console.warn('Diagnostics failed:', e); } catch { } }
|
|
2686
|
+
// Restore selection filter if we changed it
|
|
2687
|
+
if (this._diagRestoreFilter) {
|
|
2688
|
+
try { SelectionFilter.restoreAllowedSelectionTypes && SelectionFilter.restoreAllowedSelectionTypes(); } catch { }
|
|
2689
|
+
this._diagRestoreFilter = false;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
// If inspector panel is open, update it immediately for the clicked object
|
|
2693
|
+
if (this._inspectorOpen) {
|
|
2694
|
+
try { this._updateInspectorFor(target); } catch (e) { try { console.warn('Inspector update failed:', e); } catch { } }
|
|
2695
|
+
}
|
|
2696
|
+
const metadataPanel = this.__metadataPanelController;
|
|
2697
|
+
if (metadataPanel && typeof metadataPanel.handleSelection === 'function') {
|
|
2698
|
+
try { metadataPanel.handleSelection(target); }
|
|
2699
|
+
catch (e) { try { console.warn('Metadata panel update failed:', e); } catch { } }
|
|
2700
|
+
}
|
|
2701
|
+
if (triggerOnClick && typeof target.onClick === 'function') {
|
|
2702
|
+
try { target.onClick(); } catch { }
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
_clearSelectionOverlayTimer() {
|
|
2707
|
+
if (this._selectionOverlayTimer) {
|
|
2708
|
+
clearTimeout(this._selectionOverlayTimer);
|
|
2709
|
+
this._selectionOverlayTimer = null;
|
|
2710
|
+
}
|
|
2711
|
+
this._pendingSelectionOverlay = null;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
_isAssemblyChildSelection(obj) {
|
|
2715
|
+
if (!obj) return false;
|
|
2716
|
+
const type = (obj.type || '').toUpperCase();
|
|
2717
|
+
const isRefType = type === SelectionFilter.FACE || type === SelectionFilter.EDGE || type === SelectionFilter.VERTEX || type === 'POINTS';
|
|
2718
|
+
if (!isRefType) return false;
|
|
2719
|
+
const findAncestorOfType = (node, targetType) => {
|
|
2720
|
+
const norm = (t) => (t || '').toUpperCase();
|
|
2721
|
+
let cur = node?.parent || null;
|
|
2722
|
+
while (cur) {
|
|
2723
|
+
if (norm(cur.type) === norm(targetType)) return cur;
|
|
2724
|
+
cur = cur.parent || null;
|
|
2725
|
+
}
|
|
2726
|
+
return null;
|
|
2727
|
+
};
|
|
2728
|
+
const solid = findAncestorOfType(obj, SelectionFilter.SOLID);
|
|
2729
|
+
if (!solid) return false;
|
|
2730
|
+
const parent = solid.parent || null;
|
|
2731
|
+
if (!parent) return false;
|
|
2732
|
+
const normParentType = (parent.type || '').toUpperCase();
|
|
2733
|
+
const isComponent = normParentType === SelectionFilter.COMPONENT || normParentType === 'COMPONENT' || parent.isAssemblyComponent;
|
|
2734
|
+
return !!isComponent;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
_shouldDelaySelectionOverlay(candidates = []) {
|
|
2738
|
+
try {
|
|
2739
|
+
const sfAll = SelectionFilter.allowedSelectionTypes === SelectionFilter.ALL;
|
|
2740
|
+
if (!sfAll) return false;
|
|
2741
|
+
const top = Array.isArray(candidates) && candidates.length ? candidates[0].target : null;
|
|
2742
|
+
return this._isAssemblyChildSelection(top);
|
|
2743
|
+
} catch {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
_scheduleSelectionOverlay(event, candidates) {
|
|
2749
|
+
this._clearSelectionOverlayTimer();
|
|
2750
|
+
const shouldDelay = this._shouldDelaySelectionOverlay(candidates);
|
|
2751
|
+
if (!shouldDelay) {
|
|
2752
|
+
this._showSelectionOverlay(event, candidates);
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
const eventSnapshot = event ? { clientX: event.clientX, clientY: event.clientY } : null;
|
|
2756
|
+
this._pendingSelectionOverlay = { event: eventSnapshot, candidates };
|
|
2757
|
+
this._selectionOverlayTimer = setTimeout(() => {
|
|
2758
|
+
this._selectionOverlayTimer = null;
|
|
2759
|
+
const pending = this._pendingSelectionOverlay;
|
|
2760
|
+
this._pendingSelectionOverlay = null;
|
|
2761
|
+
if (pending) this._showSelectionOverlay(pending.event, pending.candidates);
|
|
2762
|
+
}, 300);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
_describeSelectionCandidate(obj) {
|
|
2766
|
+
if (!obj) return 'Selection';
|
|
2767
|
+
const name = (obj.name && String(obj.name).trim()) ? String(obj.name).trim() : null;
|
|
2768
|
+
const type = obj.type || 'object';
|
|
2769
|
+
return name || type;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
_showSelectionOverlay(event, candidates) {
|
|
2773
|
+
this._clearSelectionOverlayTimer();
|
|
2774
|
+
this._hideSelectionOverlay();
|
|
2775
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return;
|
|
2776
|
+
|
|
2777
|
+
const wrap = document.createElement('div');
|
|
2778
|
+
wrap.className = 'selection-picker';
|
|
2779
|
+
wrap.classList.add('is-hovered');
|
|
2780
|
+
const title = document.createElement('div');
|
|
2781
|
+
title.className = 'selection-picker__title selection-picker__handle';
|
|
2782
|
+
title.textContent = 'Select an object';
|
|
2783
|
+
const headerRow = document.createElement('div');
|
|
2784
|
+
headerRow.className = 'selection-picker__header';
|
|
2785
|
+
headerRow.appendChild(title);
|
|
2786
|
+
const clearBtn = document.createElement('button');
|
|
2787
|
+
clearBtn.type = 'button';
|
|
2788
|
+
clearBtn.textContent = 'Clear Selection';
|
|
2789
|
+
clearBtn.className = 'selection-picker__clear';
|
|
2790
|
+
clearBtn.addEventListener('click', (ev) => {
|
|
2791
|
+
ev.stopPropagation();
|
|
2792
|
+
ev.preventDefault();
|
|
2793
|
+
try {
|
|
2794
|
+
const scene = this.partHistory?.scene || this.scene;
|
|
2795
|
+
if (scene) SelectionFilter.unselectAll(scene);
|
|
2796
|
+
} catch { }
|
|
2797
|
+
this._hideSelectionOverlay();
|
|
2798
|
+
});
|
|
2799
|
+
headerRow.appendChild(clearBtn);
|
|
2800
|
+
wrap.appendChild(headerRow);
|
|
2801
|
+
|
|
2802
|
+
const overlayState = { wrap, drag: { active: false }, peekTimer: null };
|
|
2803
|
+
const triggerPeek = () => {
|
|
2804
|
+
if (overlayState.peekTimer) {
|
|
2805
|
+
clearTimeout(overlayState.peekTimer);
|
|
2806
|
+
overlayState.peekTimer = null;
|
|
2807
|
+
}
|
|
2808
|
+
try { wrap.style.opacity = '0.8'; } catch { }
|
|
2809
|
+
overlayState.peekTimer = setTimeout(() => {
|
|
2810
|
+
try { wrap.style.opacity = ''; } catch { }
|
|
2811
|
+
overlayState.peekTimer = null;
|
|
2812
|
+
}, 500);
|
|
2813
|
+
};
|
|
2814
|
+
|
|
2815
|
+
const list = document.createElement('div');
|
|
2816
|
+
list.className = 'selection-picker__list';
|
|
2817
|
+
const listMetrics = { itemHeight: 0, gap: 0, paddingTop: 0 };
|
|
2818
|
+
const readListStyles = () => {
|
|
2819
|
+
try {
|
|
2820
|
+
const styles = getComputedStyle(list);
|
|
2821
|
+
const gap = parseFloat(styles.rowGap || styles.gap || '0') || 0;
|
|
2822
|
+
const paddingTop = parseFloat(styles.paddingTop || '0') || 0;
|
|
2823
|
+
listMetrics.gap = gap;
|
|
2824
|
+
listMetrics.paddingTop = paddingTop;
|
|
2825
|
+
} catch { }
|
|
2826
|
+
};
|
|
2827
|
+
const ensureItemMetrics = () => {
|
|
2828
|
+
if (!listMetrics.gap && !listMetrics.paddingTop) readListStyles();
|
|
2829
|
+
if (listMetrics.itemHeight) return listMetrics.itemHeight;
|
|
2830
|
+
const first = list.querySelector('.selection-picker__item');
|
|
2831
|
+
if (!first) return 0;
|
|
2832
|
+
const rect = first.getBoundingClientRect();
|
|
2833
|
+
listMetrics.itemHeight = rect.height || first.offsetHeight || 0;
|
|
2834
|
+
return listMetrics.itemHeight;
|
|
2835
|
+
};
|
|
2836
|
+
const updateListPadding = () => {
|
|
2837
|
+
readListStyles();
|
|
2838
|
+
const first = list.querySelector('.selection-picker__item');
|
|
2839
|
+
if (!first) return;
|
|
2840
|
+
const listRect = list.getBoundingClientRect();
|
|
2841
|
+
const rect = first.getBoundingClientRect();
|
|
2842
|
+
listMetrics.itemHeight = rect.height || listMetrics.itemHeight || 0;
|
|
2843
|
+
const padding = Math.max(0, Math.round(listRect.height - listMetrics.paddingTop - rect.height));
|
|
2844
|
+
list.style.paddingBottom = `${padding}px`;
|
|
2845
|
+
};
|
|
2846
|
+
candidates.forEach((entry) => {
|
|
2847
|
+
if (!entry?.target) return;
|
|
2848
|
+
const btn = document.createElement('button');
|
|
2849
|
+
btn.type = 'button';
|
|
2850
|
+
btn.className = 'selection-picker__item';
|
|
2851
|
+
const line = document.createElement('div');
|
|
2852
|
+
line.className = 'selection-picker__line';
|
|
2853
|
+
const typeSpan = document.createElement('div');
|
|
2854
|
+
typeSpan.className = 'selection-picker__type';
|
|
2855
|
+
typeSpan.textContent = String(entry.target.type || '').toUpperCase() || 'OBJECT';
|
|
2856
|
+
const nameSpan = document.createElement('div');
|
|
2857
|
+
nameSpan.className = 'selection-picker__name';
|
|
2858
|
+
nameSpan.textContent = entry.label;
|
|
2859
|
+
line.appendChild(typeSpan);
|
|
2860
|
+
line.appendChild(nameSpan);
|
|
2861
|
+
btn.appendChild(line);
|
|
2862
|
+
btn.addEventListener('mouseenter', () => {
|
|
2863
|
+
triggerPeek();
|
|
2864
|
+
try { SelectionFilter.setHoverObject(entry.target, { ignoreFilter: true }); } catch { }
|
|
2865
|
+
});
|
|
2866
|
+
btn.addEventListener('mouseleave', () => {
|
|
2867
|
+
try { SelectionFilter.clearHover(); } catch { }
|
|
2868
|
+
});
|
|
2869
|
+
btn.addEventListener('click', (ev) => {
|
|
2870
|
+
ev.stopPropagation();
|
|
2871
|
+
ev.preventDefault?.();
|
|
2872
|
+
try {
|
|
2873
|
+
console.log('Selection picker selected:', {
|
|
2874
|
+
type: entry.target?.type,
|
|
2875
|
+
label: entry.label,
|
|
2876
|
+
target: entry.target,
|
|
2877
|
+
});
|
|
2878
|
+
} catch { /* ignore */ }
|
|
2879
|
+
this._hideSelectionOverlay();
|
|
2880
|
+
this._applySelectionTarget(entry.target);
|
|
2881
|
+
});
|
|
2882
|
+
list.appendChild(btn);
|
|
2883
|
+
});
|
|
2884
|
+
const onWheelSnapScroll = (ev) => {
|
|
2885
|
+
try { ev.preventDefault(); ev.stopPropagation(); } catch { }
|
|
2886
|
+
if (!list || list.children.length === 0) return;
|
|
2887
|
+
const dir = Math.sign(ev.deltaY || 0);
|
|
2888
|
+
if (!dir) return;
|
|
2889
|
+
const itemHeight = ensureItemMetrics();
|
|
2890
|
+
if (!itemHeight) return;
|
|
2891
|
+
const step = Math.max(1, Math.round(itemHeight + listMetrics.gap));
|
|
2892
|
+
const maxScroll = Math.max(0, list.scrollHeight - list.clientHeight);
|
|
2893
|
+
const next = Math.min(maxScroll, Math.max(0, list.scrollTop + (dir * step)));
|
|
2894
|
+
list.scrollTo({ top: next });
|
|
2895
|
+
};
|
|
2896
|
+
list.addEventListener('wheel', onWheelSnapScroll, { passive: false });
|
|
2897
|
+
wrap.appendChild(list);
|
|
2898
|
+
|
|
2899
|
+
const startX = event?.clientX ?? (window.innerWidth / 2);
|
|
2900
|
+
const startY = event?.clientY ?? (window.innerHeight / 2);
|
|
2901
|
+
wrap.style.left = `${startX}px`;
|
|
2902
|
+
wrap.style.top = `${startY}px`;
|
|
2903
|
+
|
|
2904
|
+
document.body.appendChild(wrap);
|
|
2905
|
+
|
|
2906
|
+
const adjustWithinViewport = () => {
|
|
2907
|
+
const bounds = wrap.getBoundingClientRect();
|
|
2908
|
+
const firstItem = wrap.querySelector('.selection-picker__item');
|
|
2909
|
+
let nextLeft = startX;
|
|
2910
|
+
let nextTop = startY;
|
|
2911
|
+
if (firstItem) {
|
|
2912
|
+
const firstBounds = firstItem.getBoundingClientRect();
|
|
2913
|
+
// Align pointer roughly to the center of the first item so the cursor is directly on it.
|
|
2914
|
+
const offsetX = (firstBounds.left - bounds.left) + (firstBounds.width / 2);
|
|
2915
|
+
const offsetY = (firstBounds.top - bounds.top) + (firstBounds.height / 2);
|
|
2916
|
+
nextLeft = startX - offsetX;
|
|
2917
|
+
nextTop = startY - offsetY;
|
|
2918
|
+
}
|
|
2919
|
+
const margin = 12;
|
|
2920
|
+
const width = bounds.width;
|
|
2921
|
+
const height = bounds.height;
|
|
2922
|
+
if (nextLeft + width > window.innerWidth - margin) nextLeft = Math.max(margin, window.innerWidth - width - margin);
|
|
2923
|
+
if (nextTop + height > window.innerHeight - margin) nextTop = Math.max(margin, window.innerHeight - height - margin);
|
|
2924
|
+
if (nextLeft < margin) nextLeft = margin;
|
|
2925
|
+
if (nextTop < margin) nextTop = margin;
|
|
2926
|
+
wrap.style.left = `${nextLeft}px`;
|
|
2927
|
+
wrap.style.top = `${nextTop}px`;
|
|
2928
|
+
};
|
|
2929
|
+
// Wait a frame so layout is accurate before aligning and padding the list.
|
|
2930
|
+
requestAnimationFrame(() => {
|
|
2931
|
+
updateListPadding();
|
|
2932
|
+
adjustWithinViewport();
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
const onEnter = () => {
|
|
2936
|
+
wrap.classList.add('is-hovered');
|
|
2937
|
+
};
|
|
2938
|
+
const onLeave = () => {
|
|
2939
|
+
if (!overlayState.drag.active) wrap.classList.remove('is-hovered');
|
|
2940
|
+
};
|
|
2941
|
+
|
|
2942
|
+
const onDragMove = (ev) => {
|
|
2943
|
+
if (!overlayState.drag.active) return;
|
|
2944
|
+
const margin = 12;
|
|
2945
|
+
const bounds = wrap.getBoundingClientRect();
|
|
2946
|
+
const width = bounds.width;
|
|
2947
|
+
const height = bounds.height;
|
|
2948
|
+
let nextLeft = ev.clientX - overlayState.drag.offsetX;
|
|
2949
|
+
let nextTop = ev.clientY - overlayState.drag.offsetY;
|
|
2950
|
+
if (nextLeft + width > window.innerWidth - margin) nextLeft = Math.max(margin, window.innerWidth - width - margin);
|
|
2951
|
+
if (nextTop + height > window.innerHeight - margin) nextTop = Math.max(margin, window.innerHeight - height - margin);
|
|
2952
|
+
if (nextLeft < margin) nextLeft = margin;
|
|
2953
|
+
if (nextTop < margin) nextTop = margin;
|
|
2954
|
+
wrap.style.left = `${nextLeft}px`;
|
|
2955
|
+
wrap.style.top = `${nextTop}px`;
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
const stopDrag = (ev) => {
|
|
2959
|
+
if (!overlayState.drag.active) return;
|
|
2960
|
+
overlayState.drag.active = false;
|
|
2961
|
+
wrap.classList.remove('dragging');
|
|
2962
|
+
if (!wrap.matches(':hover')) wrap.classList.remove('is-hovered');
|
|
2963
|
+
window.removeEventListener('pointermove', onDragMove, { passive: true });
|
|
2964
|
+
window.removeEventListener('pointerup', stopDrag, { passive: true, capture: true });
|
|
2965
|
+
if (ev) { try { ev.stopPropagation(); } catch { } }
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2968
|
+
const onDragStart = (ev) => {
|
|
2969
|
+
if (ev.button !== 0) return;
|
|
2970
|
+
ev.preventDefault();
|
|
2971
|
+
ev.stopPropagation();
|
|
2972
|
+
const rect = wrap.getBoundingClientRect();
|
|
2973
|
+
overlayState.drag.active = true;
|
|
2974
|
+
overlayState.drag.offsetX = ev.clientX - rect.left;
|
|
2975
|
+
overlayState.drag.offsetY = ev.clientY - rect.top;
|
|
2976
|
+
wrap.classList.add('dragging');
|
|
2977
|
+
wrap.classList.add('is-hovered');
|
|
2978
|
+
window.addEventListener('pointermove', onDragMove, { passive: true });
|
|
2979
|
+
window.addEventListener('pointerup', stopDrag, { passive: true, capture: true });
|
|
2980
|
+
};
|
|
2981
|
+
|
|
2982
|
+
title.addEventListener('pointerdown', onDragStart);
|
|
2983
|
+
wrap.addEventListener('pointerenter', onEnter);
|
|
2984
|
+
wrap.addEventListener('pointerleave', onLeave);
|
|
2985
|
+
|
|
2986
|
+
const onPointerDown = (ev) => {
|
|
2987
|
+
if (!wrap.contains(ev.target)) this._hideSelectionOverlay();
|
|
2988
|
+
};
|
|
2989
|
+
const onKey = (ev) => {
|
|
2990
|
+
if (ev.key === 'Escape') this._hideSelectionOverlay();
|
|
2991
|
+
};
|
|
2992
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
2993
|
+
document.addEventListener('keydown', onKey, true);
|
|
2994
|
+
|
|
2995
|
+
this._selectionOverlay = {
|
|
2996
|
+
wrap,
|
|
2997
|
+
onPointerDown,
|
|
2998
|
+
onKey,
|
|
2999
|
+
onEnter,
|
|
3000
|
+
onLeave,
|
|
3001
|
+
onDragStart,
|
|
3002
|
+
onDragMove,
|
|
3003
|
+
stopDrag,
|
|
3004
|
+
onWheelRotate: onWheelSnapScroll,
|
|
3005
|
+
list,
|
|
3006
|
+
overlayState,
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
_hideSelectionOverlay() {
|
|
3011
|
+
const overlay = this._selectionOverlay;
|
|
3012
|
+
if (!overlay) return;
|
|
3013
|
+
this._clearSelectionOverlayTimer();
|
|
3014
|
+
try { overlay.stopDrag?.(); } catch { }
|
|
3015
|
+
document.removeEventListener('pointerdown', overlay.onPointerDown, true);
|
|
3016
|
+
document.removeEventListener('keydown', overlay.onKey, true);
|
|
3017
|
+
try { overlay.wrap.removeEventListener('pointerenter', overlay.onEnter); } catch { }
|
|
3018
|
+
try { overlay.wrap.removeEventListener('pointerleave', overlay.onLeave); } catch { }
|
|
3019
|
+
try { overlay.wrap.querySelector('.selection-picker__handle')?.removeEventListener('pointerdown', overlay.onDragStart); } catch { }
|
|
3020
|
+
try { window.removeEventListener('pointermove', overlay.onDragMove, { passive: true }); } catch { }
|
|
3021
|
+
try { window.removeEventListener('pointerup', overlay.stopDrag, { passive: true, capture: true }); } catch { }
|
|
3022
|
+
try { overlay.list?.removeEventListener('wheel', overlay.onWheelRotate, { passive: false }); } catch { }
|
|
3023
|
+
try {
|
|
3024
|
+
if (overlay.overlayState?.peekTimer) {
|
|
3025
|
+
clearTimeout(overlay.overlayState.peekTimer);
|
|
3026
|
+
overlay.overlayState.peekTimer = null;
|
|
3027
|
+
}
|
|
3028
|
+
} catch { }
|
|
3029
|
+
try { overlay.wrap.style.opacity = ''; } catch { }
|
|
3030
|
+
try { overlay.wrap.remove(); } catch { }
|
|
3031
|
+
this._selectionOverlay = null;
|
|
3032
|
+
try { SelectionFilter.clearHover(); } catch { }
|
|
3033
|
+
// Restore hover state based on the last pointer position on the canvas
|
|
3034
|
+
try {
|
|
3035
|
+
if (this._lastPointerEvent) this._updateHover(this._lastPointerEvent);
|
|
3036
|
+
} catch { }
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// ----------------------------------------
|
|
3040
|
+
// Internal: Event Handlers
|
|
3041
|
+
// ----------------------------------------
|
|
3042
|
+
_onPointerMove(event) {
|
|
3043
|
+
if (this._disposed) return;
|
|
3044
|
+
// Keep last pointer position and refresh hover
|
|
3045
|
+
this._lastPointerEvent = event;
|
|
3046
|
+
// If hovering over the view cube, avoid main-scene hover
|
|
3047
|
+
try {
|
|
3048
|
+
if (this.viewCube && this.viewCube.isEventInside(event)) return;
|
|
3049
|
+
} catch { }
|
|
3050
|
+
// If hovering TransformControls gizmo, skip scene hover handling
|
|
3051
|
+
try {
|
|
3052
|
+
const ax = (typeof window !== 'undefined') ? (window.__BREP_activeXform || null) : null;
|
|
3053
|
+
if (ax && typeof ax.isOver === 'function' && ax.isOver(event)) return;
|
|
3054
|
+
} catch { }
|
|
3055
|
+
this._updateHover(event);
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
_onPointerDown(event) {
|
|
3059
|
+
if (this._disposed) return;
|
|
3060
|
+
this._hideSelectionOverlay();
|
|
3061
|
+
// If pointer is over TransformControls gizmo, let it handle the interaction
|
|
3062
|
+
try {
|
|
3063
|
+
const ax = (typeof window !== 'undefined') ? (window.__BREP_activeXform || null) : null;
|
|
3064
|
+
if (ax && typeof ax.isOver === 'function' && ax.isOver(event)) { try { event.preventDefault(); } catch { }; return; }
|
|
3065
|
+
} catch { }
|
|
3066
|
+
this._clearSelectionOverlayTimer();
|
|
3067
|
+
try {
|
|
3068
|
+
if (this._isEventOverRenderer(event)) {
|
|
3069
|
+
this._lastCanvasPointerDownAt = Date.now();
|
|
3070
|
+
}
|
|
3071
|
+
} catch { }
|
|
3072
|
+
// If pressing in the view cube region, disable controls for this gesture
|
|
3073
|
+
try {
|
|
3074
|
+
this._cubeActive = !!(this.viewCube && this.viewCube.isEventInside(event));
|
|
3075
|
+
} catch { this._cubeActive = false; }
|
|
3076
|
+
this._pointerDown = true;
|
|
3077
|
+
this._downButton = event.button;
|
|
3078
|
+
this._downPos.x = event.clientX;
|
|
3079
|
+
this._downPos.y = event.clientY;
|
|
3080
|
+
this.controls.enabled = !this._cubeActive;
|
|
3081
|
+
// Prevent default to avoid unwanted text selection/scroll on drag
|
|
3082
|
+
try { event.preventDefault(); } catch { }
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
_onPointerUp(event) {
|
|
3086
|
+
if (this._disposed) return;
|
|
3087
|
+
// If releasing over TransformControls gizmo, skip scene selection
|
|
3088
|
+
try {
|
|
3089
|
+
const ax = (typeof window !== 'undefined') ? (window.__BREP_activeXform || null) : null;
|
|
3090
|
+
if (ax && typeof ax.isOver === 'function' && ax.isOver(event)) { try { event.preventDefault(); } catch { }; return; }
|
|
3091
|
+
} catch { }
|
|
3092
|
+
// If the gesture began in the cube, handle click there exclusively
|
|
3093
|
+
if (this._cubeActive) {
|
|
3094
|
+
try { if (this.viewCube && this.viewCube.handleClick(event)) { this._cubeActive = false; return; } } catch { }
|
|
3095
|
+
this._cubeActive = false;
|
|
3096
|
+
}
|
|
3097
|
+
// Click selection if within drag threshold and left button
|
|
3098
|
+
const dx = Math.abs(event.clientX - this._downPos.x);
|
|
3099
|
+
const dy = Math.abs(event.clientY - this._downPos.y);
|
|
3100
|
+
const moved = (dx + dy) > this._dragThreshold;
|
|
3101
|
+
if (this._pointerDown && this._downButton === 0 && !moved) {
|
|
3102
|
+
this._selectAt(event);
|
|
3103
|
+
}
|
|
3104
|
+
// Reset flags and keep controls enabled
|
|
3105
|
+
this._pointerDown = false;
|
|
3106
|
+
this.controls.enabled = true;
|
|
3107
|
+
void event;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
_onContextMenu(event) {
|
|
3111
|
+
// No interactive targets; allow default context menu
|
|
3112
|
+
void event;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
_onKeyDown(event) {
|
|
3116
|
+
if (this._disposed) return;
|
|
3117
|
+
const target = event?.target || null;
|
|
3118
|
+
const tag = target?.tagName ? String(target.tagName).toLowerCase() : '';
|
|
3119
|
+
const isEditable = !!(
|
|
3120
|
+
target
|
|
3121
|
+
&& (target.isContentEditable
|
|
3122
|
+
|| tag === 'input'
|
|
3123
|
+
|| tag === 'textarea'
|
|
3124
|
+
|| tag === 'select')
|
|
3125
|
+
);
|
|
3126
|
+
const key = (event?.key || '').toLowerCase();
|
|
3127
|
+
const isMod = !!(event?.ctrlKey || event?.metaKey);
|
|
3128
|
+
const isUndo = isMod && !event?.altKey && key === 'z' && !event?.shiftKey;
|
|
3129
|
+
const isRedo = isMod && !event?.altKey && (key === 'y' || (event?.shiftKey && key === 'z'));
|
|
3130
|
+
if ((isUndo || isRedo) && !isEditable) {
|
|
3131
|
+
if (this._imageEditorActive) return;
|
|
3132
|
+
try {
|
|
3133
|
+
if (this._sketchMode && typeof this._sketchMode.undo === 'function' && typeof this._sketchMode.redo === 'function') {
|
|
3134
|
+
if (isUndo) this._sketchMode.undo();
|
|
3135
|
+
else this._sketchMode.redo();
|
|
3136
|
+
} else if (this.partHistory) {
|
|
3137
|
+
void this._runFeatureHistoryUndoRedo(isRedo ? 'redo' : 'undo');
|
|
3138
|
+
}
|
|
3139
|
+
try { event.preventDefault(); } catch { }
|
|
3140
|
+
try { event.stopImmediatePropagation(); } catch { }
|
|
3141
|
+
} catch { }
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
const k = event?.key || event?.code || '';
|
|
3145
|
+
if (k === 'Escape' || k === 'Esc') {
|
|
3146
|
+
try { this._hideSelectionOverlay(); } catch { }
|
|
3147
|
+
try {
|
|
3148
|
+
const scene = this.partHistory?.scene || this.scene;
|
|
3149
|
+
if (scene) {
|
|
3150
|
+
SelectionFilter.unselectAll(scene);
|
|
3151
|
+
SelectionFilter.restoreAllowedSelectionTypes();
|
|
3152
|
+
}
|
|
3153
|
+
} catch { }
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
_findOwningComponent(obj) {
|
|
3158
|
+
let cur = obj;
|
|
3159
|
+
while (cur) {
|
|
3160
|
+
if (cur.isAssemblyComponent || cur.type === SelectionFilter.COMPONENT || cur.type === 'COMPONENT') {
|
|
3161
|
+
return cur;
|
|
3162
|
+
}
|
|
3163
|
+
cur = cur.parent;
|
|
3164
|
+
}
|
|
3165
|
+
return null;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
_stopComponentTransformSession() {
|
|
3169
|
+
const session = this._componentTransformSession;
|
|
3170
|
+
if (!session) return;
|
|
3171
|
+
const { controls, helper, target, changeHandler, dragHandler, objectChangeHandler, globalState } = session;
|
|
3172
|
+
|
|
3173
|
+
try { controls?.removeEventListener('change', changeHandler); } catch { }
|
|
3174
|
+
try { controls?.removeEventListener('dragging-changed', dragHandler); } catch { }
|
|
3175
|
+
try { controls?.removeEventListener('objectChange', objectChangeHandler); } catch { }
|
|
3176
|
+
|
|
3177
|
+
try { controls?.detach?.(); } catch { }
|
|
3178
|
+
|
|
3179
|
+
if (this.scene) {
|
|
3180
|
+
try { if (controls && controls.isObject3D) this.scene.remove(controls); } catch { }
|
|
3181
|
+
try { if (helper && helper.isObject3D) this.scene.remove(helper); } catch { }
|
|
3182
|
+
try { if (target && target.isObject3D) this.scene.remove(target); } catch { }
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
try { controls?.dispose?.(); } catch { }
|
|
3186
|
+
|
|
3187
|
+
try {
|
|
3188
|
+
if (window.__BREP_activeXform === globalState) {
|
|
3189
|
+
window.__BREP_activeXform = null;
|
|
3190
|
+
}
|
|
3191
|
+
} catch { }
|
|
3192
|
+
|
|
3193
|
+
this._componentTransformSession = null;
|
|
3194
|
+
try { if (this.controls) this.controls.enabled = true; } catch { }
|
|
3195
|
+
try { this.render(); } catch { }
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
_activateComponentTransform(component) {
|
|
3199
|
+
if (!component) return;
|
|
3200
|
+
if (component.fixed) return;
|
|
3201
|
+
const TCctor = CombinedTransformControls;
|
|
3202
|
+
if (!TCctor) {
|
|
3203
|
+
console.warn('[Viewer] TransformControls unavailable; cannot activate component gizmo.');
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
this._stopComponentTransformSession();
|
|
3208
|
+
try { if (SchemaForm && typeof SchemaForm.__stopGlobalActiveXform === 'function') SchemaForm.__stopGlobalActiveXform(); } catch { }
|
|
3209
|
+
|
|
3210
|
+
const controls = new TCctor(this.camera, this.renderer.domElement);
|
|
3211
|
+
const initialMode = 'translate';
|
|
3212
|
+
try { controls.setMode(initialMode); } catch { controls.mode = initialMode; }
|
|
3213
|
+
try { controls.showX = controls.showY = controls.showZ = true; } catch { }
|
|
3214
|
+
|
|
3215
|
+
const target = new THREE.Object3D();
|
|
3216
|
+
target.name = `ComponentTransformTarget:${component.name || component.uuid || ''}`;
|
|
3217
|
+
|
|
3218
|
+
try { this.scene.updateMatrixWorld?.(true); } catch { }
|
|
3219
|
+
try { component.updateMatrixWorld?.(true); } catch { }
|
|
3220
|
+
|
|
3221
|
+
const box = new THREE.Box3();
|
|
3222
|
+
const center = box.setFromObject(component).isEmpty()
|
|
3223
|
+
? component.getWorldPosition(new THREE.Vector3())
|
|
3224
|
+
: box.getCenter(new THREE.Vector3());
|
|
3225
|
+
target.position.copy(center);
|
|
3226
|
+
|
|
3227
|
+
const componentWorldQuat = component.getWorldQuaternion(new THREE.Quaternion());
|
|
3228
|
+
target.quaternion.copy(componentWorldQuat);
|
|
3229
|
+
|
|
3230
|
+
const parent = component.parent || this.scene;
|
|
3231
|
+
try { parent?.updateMatrixWorld?.(true); } catch { }
|
|
3232
|
+
|
|
3233
|
+
const offsetLocal = component.getWorldPosition(new THREE.Vector3()).sub(center);
|
|
3234
|
+
const initialTargetQuatInv = componentWorldQuat.clone().invert();
|
|
3235
|
+
offsetLocal.applyQuaternion(initialTargetQuatInv);
|
|
3236
|
+
|
|
3237
|
+
const parentInverse = new THREE.Matrix4();
|
|
3238
|
+
if (parent && parent.isObject3D) {
|
|
3239
|
+
parentInverse.copy(parent.matrixWorld).invert();
|
|
3240
|
+
} else {
|
|
3241
|
+
parentInverse.identity();
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
this.scene.add(target);
|
|
3245
|
+
try { controls.attach(target); } catch { }
|
|
3246
|
+
try {
|
|
3247
|
+
controls.userData = controls.userData || {};
|
|
3248
|
+
controls.userData.excludeFromFit = true;
|
|
3249
|
+
this.scene.add(controls);
|
|
3250
|
+
} catch { }
|
|
3251
|
+
|
|
3252
|
+
let helper = null;
|
|
3253
|
+
try {
|
|
3254
|
+
helper = typeof controls.getHelper === 'function' ? controls.getHelper() : null;
|
|
3255
|
+
if (helper && helper.isObject3D) {
|
|
3256
|
+
helper.userData = helper.userData || {};
|
|
3257
|
+
helper.userData.excludeFromFit = true;
|
|
3258
|
+
this.scene.add(helper);
|
|
3259
|
+
}
|
|
3260
|
+
} catch { helper = null; }
|
|
3261
|
+
|
|
3262
|
+
const markOverlay = (obj) => {
|
|
3263
|
+
if (!obj || !obj.isObject3D) return;
|
|
3264
|
+
const apply = (node) => {
|
|
3265
|
+
if (!node || !node.isObject3D) return;
|
|
3266
|
+
const ud = node.userData || (node.userData = {});
|
|
3267
|
+
if (ud.__brepOverlayHook) return;
|
|
3268
|
+
const prev = node.onBeforeRender;
|
|
3269
|
+
node.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
|
|
3270
|
+
try { renderer.clearDepth(); } catch { }
|
|
3271
|
+
if (typeof prev === 'function') {
|
|
3272
|
+
prev.call(this, renderer, scene, camera, geometry, material, group);
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
ud.__brepOverlayHook = true;
|
|
3276
|
+
};
|
|
3277
|
+
apply(obj);
|
|
3278
|
+
try { obj.traverse((child) => apply(child)); } catch { }
|
|
3279
|
+
};
|
|
3280
|
+
try { markOverlay(controls); } catch { }
|
|
3281
|
+
try { markOverlay(helper); } catch { }
|
|
3282
|
+
try { markOverlay(controls?._gizmo); } catch { }
|
|
3283
|
+
try { markOverlay(controls?.gizmo); } catch { }
|
|
3284
|
+
|
|
3285
|
+
const scratchTargetWorld = new THREE.Vector3();
|
|
3286
|
+
const scratchComponentWorld = new THREE.Vector3();
|
|
3287
|
+
const scratchLocal = new THREE.Vector3();
|
|
3288
|
+
const scratchRotatedOffset = new THREE.Vector3();
|
|
3289
|
+
const scratchTargetQuat = new THREE.Quaternion();
|
|
3290
|
+
const scratchParentQuat = new THREE.Quaternion();
|
|
3291
|
+
const scratchParentQuatInv = new THREE.Quaternion();
|
|
3292
|
+
const scratchComponentQuat = new THREE.Quaternion();
|
|
3293
|
+
|
|
3294
|
+
const updateComponentTransform = (commit = false) => {
|
|
3295
|
+
try {
|
|
3296
|
+
try { this.scene.updateMatrixWorld?.(true); } catch { }
|
|
3297
|
+
try { target.updateMatrixWorld?.(true); } catch { }
|
|
3298
|
+
if (parent && parent.isObject3D) {
|
|
3299
|
+
try { parent.updateMatrixWorld?.(true); } catch { }
|
|
3300
|
+
parentInverse.copy(parent.matrixWorld).invert();
|
|
3301
|
+
parent.getWorldQuaternion(scratchParentQuat);
|
|
3302
|
+
scratchParentQuatInv.copy(scratchParentQuat).invert();
|
|
3303
|
+
} else {
|
|
3304
|
+
parentInverse.identity();
|
|
3305
|
+
scratchParentQuat.set(0, 0, 0, 1);
|
|
3306
|
+
scratchParentQuatInv.copy(scratchParentQuat);
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
target.getWorldPosition(scratchTargetWorld);
|
|
3310
|
+
target.getWorldQuaternion(scratchTargetQuat);
|
|
3311
|
+
|
|
3312
|
+
scratchRotatedOffset.copy(offsetLocal).applyQuaternion(scratchTargetQuat);
|
|
3313
|
+
scratchComponentWorld.copy(scratchTargetWorld).add(scratchRotatedOffset);
|
|
3314
|
+
scratchLocal.copy(scratchComponentWorld);
|
|
3315
|
+
if (parent && parent.isObject3D) {
|
|
3316
|
+
scratchLocal.applyMatrix4(parentInverse);
|
|
3317
|
+
}
|
|
3318
|
+
component.position.copy(scratchLocal);
|
|
3319
|
+
if (parent && parent.isObject3D) {
|
|
3320
|
+
scratchComponentQuat.copy(scratchParentQuatInv).multiply(scratchTargetQuat);
|
|
3321
|
+
component.quaternion.copy(scratchComponentQuat);
|
|
3322
|
+
} else {
|
|
3323
|
+
component.quaternion.copy(scratchTargetQuat);
|
|
3324
|
+
}
|
|
3325
|
+
component.updateMatrixWorld?.(true);
|
|
3326
|
+
this.render();
|
|
3327
|
+
if (commit && this.partHistory && typeof this.partHistory.syncAssemblyComponentTransforms === 'function') {
|
|
3328
|
+
this.partHistory.syncAssemblyComponentTransforms();
|
|
3329
|
+
}
|
|
3330
|
+
} catch (err) {
|
|
3331
|
+
console.warn('[Viewer] Failed to apply transform to component:', err);
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
|
|
3335
|
+
const changeHandler = () => { updateComponentTransform(false); };
|
|
3336
|
+
const dragHandler = (ev) => {
|
|
3337
|
+
const dragging = !!(ev && ev.value);
|
|
3338
|
+
try { if (this.controls) this.controls.enabled = !dragging; } catch { }
|
|
3339
|
+
if (!dragging) updateComponentTransform(true);
|
|
3340
|
+
};
|
|
3341
|
+
const objectChangeHandler = () => {
|
|
3342
|
+
if (!controls || controls.dragging) return;
|
|
3343
|
+
updateComponentTransform(true);
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
controls.addEventListener('change', changeHandler);
|
|
3347
|
+
controls.addEventListener('dragging-changed', dragHandler);
|
|
3348
|
+
try { controls.addEventListener('objectChange', objectChangeHandler); } catch { }
|
|
3349
|
+
|
|
3350
|
+
const isOver = (ev) => {
|
|
3351
|
+
try {
|
|
3352
|
+
if (!ev) return false;
|
|
3353
|
+
const ndc = this._getPointerNDC(ev);
|
|
3354
|
+
this.raycaster.setFromCamera(ndc, this.camera);
|
|
3355
|
+
const mode = (typeof controls.getMode === 'function') ? controls.getMode() : (controls.mode || 'translate');
|
|
3356
|
+
const giz = controls._gizmo || controls.gizmo || null;
|
|
3357
|
+
const pickRoot = (giz && giz.picker) ? (giz.picker[mode] || giz.picker.translate || giz.picker.rotate || giz.picker.scale) : giz;
|
|
3358
|
+
const root = pickRoot || giz || helper || controls;
|
|
3359
|
+
if (!root) return false;
|
|
3360
|
+
const hits = this.raycaster.intersectObject(root, true) || [];
|
|
3361
|
+
return hits.length > 0;
|
|
3362
|
+
} catch { return false; }
|
|
3363
|
+
};
|
|
3364
|
+
|
|
3365
|
+
const updateForCamera = () => {
|
|
3366
|
+
try {
|
|
3367
|
+
if (typeof controls.update === 'function') controls.update();
|
|
3368
|
+
else controls.updateMatrixWorld(true);
|
|
3369
|
+
} catch { }
|
|
3370
|
+
};
|
|
3371
|
+
|
|
3372
|
+
const globalState = {
|
|
3373
|
+
controls,
|
|
3374
|
+
viewer: this,
|
|
3375
|
+
target,
|
|
3376
|
+
isOver,
|
|
3377
|
+
updateForCamera,
|
|
3378
|
+
};
|
|
3379
|
+
try { window.__BREP_activeXform = globalState; } catch { }
|
|
3380
|
+
|
|
3381
|
+
const sessionMode = (typeof controls.getMode === 'function') ? controls.getMode() : (controls.mode || initialMode);
|
|
3382
|
+
|
|
3383
|
+
this._componentTransformSession = {
|
|
3384
|
+
component,
|
|
3385
|
+
controls,
|
|
3386
|
+
helper,
|
|
3387
|
+
target,
|
|
3388
|
+
changeHandler,
|
|
3389
|
+
dragHandler,
|
|
3390
|
+
objectChangeHandler,
|
|
3391
|
+
globalState,
|
|
3392
|
+
mode: sessionMode,
|
|
3393
|
+
};
|
|
3394
|
+
|
|
3395
|
+
updateComponentTransform(false);
|
|
3396
|
+
this.render();
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
_onGlobalDoubleClick(event) {
|
|
3400
|
+
if (this._disposed) return;
|
|
3401
|
+
const lastDownAge = Date.now() - (this._lastCanvasPointerDownAt || 0);
|
|
3402
|
+
// Only honor double-clicks that are closely preceded by a canvas pointerdown,
|
|
3403
|
+
// even if the second click lands on the selection popover.
|
|
3404
|
+
if (!Number.isFinite(lastDownAge) || lastDownAge > 750) return;
|
|
3405
|
+
this._clearSelectionOverlayTimer();
|
|
3406
|
+
this._onDoubleClick(event);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
_onDoubleClick(event) {
|
|
3410
|
+
if (this._disposed) return;
|
|
3411
|
+
if (event && event.__brepHandledDblclick) return;
|
|
3412
|
+
if (event) event.__brepHandledDblclick = true;
|
|
3413
|
+
try { event?.preventDefault?.(); } catch { }
|
|
3414
|
+
try {
|
|
3415
|
+
this._clearSelectionOverlayTimer();
|
|
3416
|
+
this._hideSelectionOverlay();
|
|
3417
|
+
} catch { }
|
|
3418
|
+
try {
|
|
3419
|
+
const ax = window.__BREP_activeXform;
|
|
3420
|
+
if (ax && typeof ax.isOver === 'function' && ax.isOver(event)) return;
|
|
3421
|
+
} catch { }
|
|
3422
|
+
|
|
3423
|
+
const pick = this._pickAtEvent(event, { ignoreSelectionFilter: true, allowAnyAllowedType: true });
|
|
3424
|
+
const component = pick && pick.target ? this._findOwningComponent(pick.target) : null;
|
|
3425
|
+
|
|
3426
|
+
if (!component) {
|
|
3427
|
+
this._stopComponentTransformSession();
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
if (component.fixed) {
|
|
3432
|
+
try {
|
|
3433
|
+
if (typeof this._toast === 'function') this._toast('Component is fixed and cannot be moved.');
|
|
3434
|
+
} catch { }
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
const session = this._componentTransformSession;
|
|
3439
|
+
if (session && session.component === component) {
|
|
3440
|
+
const controls = session.controls;
|
|
3441
|
+
const currentMode = (typeof controls?.getMode === 'function') ? controls.getMode() : (controls?.mode || session.mode || 'translate');
|
|
3442
|
+
if (currentMode === 'translate') {
|
|
3443
|
+
const nextMode = 'rotate';
|
|
3444
|
+
try { controls?.setMode(nextMode); } catch { if (controls) controls.mode = nextMode; }
|
|
3445
|
+
session.mode = nextMode;
|
|
3446
|
+
try { session.globalState?.updateForCamera?.(); } catch { }
|
|
3447
|
+
try { this.render(); } catch { }
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
if (currentMode === 'rotate') {
|
|
3451
|
+
this._stopComponentTransformSession();
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
this._stopComponentTransformSession();
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
this._activateComponentTransform(component);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// ----------------------------------------
|
|
3462
|
+
// Diagnostics (one‑shot picker)
|
|
3463
|
+
// ----------------------------------------
|
|
3464
|
+
enableDiagnosticPick() {
|
|
3465
|
+
this._diagPickOnce = true;
|
|
3466
|
+
// Do not modify the SelectionFilter; inspect will honor the current filter.
|
|
3467
|
+
try { this._toast('Click an item to inspect'); } catch { }
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// ----------------------------------------
|
|
3471
|
+
// Inspector panel (toggle + update-on-click)
|
|
3472
|
+
// ----------------------------------------
|
|
3473
|
+
toggleInspectorPanel() { this._inspectorOpen ? this._closeInspectorPanel() : this._openInspectorPanel(); }
|
|
3474
|
+
_openInspectorPanel() {
|
|
3475
|
+
if (this._inspectorOpen) return;
|
|
3476
|
+
this._ensureInspectorPanel();
|
|
3477
|
+
this._inspectorEl.style.display = 'flex';
|
|
3478
|
+
this._inspectorOpen = true;
|
|
3479
|
+
// Placeholder message until user clicks an object
|
|
3480
|
+
try {
|
|
3481
|
+
this._setInspectorPlaceholder('Click an object in the scene to inspect.');
|
|
3482
|
+
} catch { }
|
|
3483
|
+
}
|
|
3484
|
+
_closeInspectorPanel() {
|
|
3485
|
+
if (!this._inspectorOpen) return;
|
|
3486
|
+
this._inspectorOpen = false;
|
|
3487
|
+
try { this._inspectorEl.style.display = 'none'; } catch { }
|
|
3488
|
+
}
|
|
3489
|
+
_ensureInspectorPanel() {
|
|
3490
|
+
if (this._inspectorEl) return;
|
|
3491
|
+
// Create a floating window anchored bottom-left, resizable and draggable
|
|
3492
|
+
const height = Math.max(260, Math.floor((window?.innerHeight || 800) * 0.7));
|
|
3493
|
+
const fw = new FloatingWindow({
|
|
3494
|
+
title: 'Inspector',
|
|
3495
|
+
width: 520,
|
|
3496
|
+
height,
|
|
3497
|
+
x: 12,
|
|
3498
|
+
bottom: 12,
|
|
3499
|
+
shaded: false,
|
|
3500
|
+
onClose: () => this._closeInspectorPanel(),
|
|
3501
|
+
});
|
|
3502
|
+
// Header actions
|
|
3503
|
+
const btnTriangles = document.createElement('button');
|
|
3504
|
+
btnTriangles.className = 'fw-btn';
|
|
3505
|
+
btnTriangles.textContent = 'Triangle Debugger';
|
|
3506
|
+
btnTriangles.title = 'Open triangle debugger for the current selection';
|
|
3507
|
+
btnTriangles.addEventListener('click', () => {
|
|
3508
|
+
try { this._openTriangleDebugger(); }
|
|
3509
|
+
catch (e) { try { console.warn('Triangle debugger failed:', e); } catch { } }
|
|
3510
|
+
});
|
|
3511
|
+
fw.addHeaderAction(btnTriangles);
|
|
3512
|
+
|
|
3513
|
+
const btnDownload = document.createElement('button');
|
|
3514
|
+
btnDownload.className = 'fw-btn';
|
|
3515
|
+
btnDownload.textContent = 'Download JSON';
|
|
3516
|
+
btnDownload.addEventListener('click', () => {
|
|
3517
|
+
try {
|
|
3518
|
+
const json = this._lastInspectorDownload ? this._lastInspectorDownload() : (this._lastInspectorJSON || '{}');
|
|
3519
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
3520
|
+
const url = URL.createObjectURL(blob);
|
|
3521
|
+
const a = document.createElement('a'); a.href = url; a.download = 'diagnostics.json'; document.body.appendChild(a); a.click();
|
|
3522
|
+
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
|
|
3523
|
+
} catch { }
|
|
3524
|
+
});
|
|
3525
|
+
fw.addHeaderAction(btnDownload);
|
|
3526
|
+
|
|
3527
|
+
// Wire content area
|
|
3528
|
+
const content = document.createElement('div');
|
|
3529
|
+
content.style.display = 'block';
|
|
3530
|
+
content.style.width = '100%';
|
|
3531
|
+
content.style.height = '100%';
|
|
3532
|
+
fw.content.appendChild(content);
|
|
3533
|
+
|
|
3534
|
+
this._inspectorFW = fw;
|
|
3535
|
+
this._inspectorEl = fw.root;
|
|
3536
|
+
this._inspectorContent = content;
|
|
3537
|
+
this._lastInspectorDownload = null;
|
|
3538
|
+
this._lastInspectorJSON = '{}';
|
|
3539
|
+
}
|
|
3540
|
+
_setInspectorPlaceholder(msg) {
|
|
3541
|
+
if (!this._inspectorContent) return;
|
|
3542
|
+
this._inspectorContent.innerHTML = '';
|
|
3543
|
+
const p = document.createElement('div');
|
|
3544
|
+
p.textContent = msg || '';
|
|
3545
|
+
p.style.color = '#9aa4b2';
|
|
3546
|
+
p.style.font = '12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
3547
|
+
p.style.opacity = '0.9';
|
|
3548
|
+
this._inspectorContent.appendChild(p);
|
|
3549
|
+
this._lastInspectorDownload = null;
|
|
3550
|
+
this._lastInspectorJSON = '{}';
|
|
3551
|
+
}
|
|
3552
|
+
_updateInspectorFor(target) {
|
|
3553
|
+
this._ensureInspectorPanel();
|
|
3554
|
+
this._lastInspectorTarget = target || null;
|
|
3555
|
+
this._lastInspectorSolid = this._findParentSolid(target);
|
|
3556
|
+
if (this._triangleDebugger && this._triangleDebugger.isOpen && this._triangleDebugger.isOpen()) {
|
|
3557
|
+
try { this._triangleDebugger.refreshTarget(target); } catch { }
|
|
3558
|
+
}
|
|
3559
|
+
if (!target) { this._setInspectorPlaceholder('Nothing selected.'); return; }
|
|
3560
|
+
try {
|
|
3561
|
+
const { out, downloadFactory } = this._buildDiagnostics(target);
|
|
3562
|
+
this._inspectorContent.innerHTML = '';
|
|
3563
|
+
// Attach object UI tree
|
|
3564
|
+
const ui = generateObjectUI(out, { title: 'Object Inspector', showTypes: true, collapseChildren: true });
|
|
3565
|
+
this._inspectorContent.appendChild(ui);
|
|
3566
|
+
// Persist download factory and raw JSON for header button
|
|
3567
|
+
this._lastInspectorDownload = downloadFactory;
|
|
3568
|
+
this._lastInspectorJSON = JSON.stringify(out, null, 2);
|
|
3569
|
+
} catch (e) {
|
|
3570
|
+
console.warn(e);
|
|
3571
|
+
this._setInspectorPlaceholder('Inspector failed. See console.');
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
_getTriangleDebugger() {
|
|
3576
|
+
if (!this._triangleDebugger) {
|
|
3577
|
+
this._triangleDebugger = new TriangleDebuggerWindow({ viewer: this });
|
|
3578
|
+
}
|
|
3579
|
+
return this._triangleDebugger;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
_openTriangleDebugger() {
|
|
3583
|
+
try {
|
|
3584
|
+
const dbg = this._getTriangleDebugger();
|
|
3585
|
+
dbg.openFor(this._lastInspectorTarget || this._lastInspectorSolid || null);
|
|
3586
|
+
} catch (e) {
|
|
3587
|
+
try { console.warn('Triangle debugger open failed:', e); } catch { }
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
_findParentSolid(obj) {
|
|
3592
|
+
const isSolid = (node) => node && (String(node.type || '').toUpperCase() === 'SOLID');
|
|
3593
|
+
let cur = obj || null;
|
|
3594
|
+
if (cur && cur.parentSolid && isSolid(cur.parentSolid)) return cur.parentSolid;
|
|
3595
|
+
if (cur && cur.userData && cur.userData.parentSolid && isSolid(cur.userData.parentSolid)) return cur.userData.parentSolid;
|
|
3596
|
+
while (cur) {
|
|
3597
|
+
if (isSolid(cur)) return cur;
|
|
3598
|
+
if (cur.parentSolid && isSolid(cur.parentSolid)) return cur.parentSolid;
|
|
3599
|
+
if (cur.userData && cur.userData.parentSolid && isSolid(cur.userData.parentSolid)) return cur.userData.parentSolid;
|
|
3600
|
+
cur = cur.parent || null;
|
|
3601
|
+
}
|
|
3602
|
+
return null;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
_round(n) { return Math.abs(n) < 1e-12 ? 0 : Number(n.toFixed(6)); }
|
|
3606
|
+
|
|
3607
|
+
_edgePointsWorld(edge) {
|
|
3608
|
+
const pts = [];
|
|
3609
|
+
const v = new THREE.Vector3();
|
|
3610
|
+
const local = edge?.userData?.polylineLocal;
|
|
3611
|
+
const isWorld = !!(edge?.userData?.polylineWorld);
|
|
3612
|
+
if (Array.isArray(local) && local.length >= 2) {
|
|
3613
|
+
if (isWorld) {
|
|
3614
|
+
for (const p of local) pts.push([this._round(p[0]), this._round(p[1]), this._round(p[2])]);
|
|
3615
|
+
} else {
|
|
3616
|
+
for (const p of local) { v.set(p[0], p[1], p[2]).applyMatrix4(edge.matrixWorld); pts.push([this._round(v.x), this._round(v.y), this._round(v.z)]); }
|
|
3617
|
+
}
|
|
3618
|
+
} else {
|
|
3619
|
+
const pos = edge?.geometry?.getAttribute?.('position');
|
|
3620
|
+
if (pos && pos.itemSize === 3) {
|
|
3621
|
+
for (let i = 0; i < pos.count; i++) { v.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(edge.matrixWorld); pts.push([this._round(v.x), this._round(v.y), this._round(v.z)]); }
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
return pts;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
_buildDiagnostics(target) {
|
|
3628
|
+
const out = { type: target?.type || String(target?.constructor?.name || 'Object'), name: target?.name || null };
|
|
3629
|
+
let downloadFactory = null; // optional closure that returns full JSON text for download
|
|
3630
|
+
|
|
3631
|
+
// Add owning feature information if available
|
|
3632
|
+
try {
|
|
3633
|
+
if (target.owningFeatureID) {
|
|
3634
|
+
out.owningFeatureID = target.owningFeatureID;
|
|
3635
|
+
out._owningFeatureFormatted = `Created by: ${target.owningFeatureID}`;
|
|
3636
|
+
} else if (target.parentSolid && target.parentSolid.owningFeatureID) {
|
|
3637
|
+
out.owningFeatureID = target.parentSolid.owningFeatureID;
|
|
3638
|
+
out._owningFeatureFormatted = `Created by: ${target.parentSolid.owningFeatureID}`;
|
|
3639
|
+
}
|
|
3640
|
+
} catch { }
|
|
3641
|
+
|
|
3642
|
+
if (target.type === 'FACE') {
|
|
3643
|
+
// Triangles via Solid API to ensure correct grouping
|
|
3644
|
+
let solid = target.parent; while (solid && solid.type !== 'SOLID') solid = solid.parent;
|
|
3645
|
+
const faceName = target.userData?.faceName || target.name;
|
|
3646
|
+
try {
|
|
3647
|
+
if (solid && typeof solid.getFace === 'function' && faceName) {
|
|
3648
|
+
const tris = solid.getFace(faceName) || [];
|
|
3649
|
+
const mapTri = (t) => ({
|
|
3650
|
+
indices: Array.isArray(t.indices) ? t.indices : undefined,
|
|
3651
|
+
p1: t.p1.map(this._round), p2: t.p2.map(this._round), p3: t.p3.map(this._round),
|
|
3652
|
+
normal: (() => { const a = t.p1, b = t.p2, c = t.p3; const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2]; const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2]; const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx; const len = Math.hypot(nx, ny, nz) || 1; return [this._round(nx / len), this._round(ny / len), this._round(nz / len)]; })(),
|
|
3653
|
+
area: (() => { const a = t.p1, b = t.p2, c = t.p3; const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2]; const vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2]; const cx = uy * vz - uz * vy, cy = uz * vx - ux * vz, cz = ux * vy - uy * vx; return this._round(0.5 * Math.hypot(cx, cy, cz)); })()
|
|
3654
|
+
});
|
|
3655
|
+
const triFull = tris.map(mapTri);
|
|
3656
|
+
try {
|
|
3657
|
+
let triMax = 5000; // preview cap
|
|
3658
|
+
if (typeof window !== 'undefined' && Number.isFinite(window.BREP_DIAG_TRI_MAX_FACE)) triMax = window.BREP_DIAG_TRI_MAX_FACE | 0;
|
|
3659
|
+
if (triMax < 0) triMax = triFull.length;
|
|
3660
|
+
const count = Math.min(triFull.length, triMax);
|
|
3661
|
+
// Make triangles lazy-loaded for performance
|
|
3662
|
+
out._trianglesSummary = `${triFull.length} triangles (click to expand)`;
|
|
3663
|
+
out._lazyTriangles = () => triFull.slice(0, count);
|
|
3664
|
+
if (count < triFull.length) { out.trianglesTruncated = true; out.trianglesTotal = triFull.length; out.trianglesLimit = triMax; }
|
|
3665
|
+
} catch {
|
|
3666
|
+
out._trianglesSummary = `${triFull.length} triangles (click to expand)`;
|
|
3667
|
+
out._lazyTriangles = () => triFull;
|
|
3668
|
+
}
|
|
3669
|
+
// Full JSON factory for download
|
|
3670
|
+
downloadFactory = () => {
|
|
3671
|
+
const full = JSON.parse(JSON.stringify(out));
|
|
3672
|
+
full.triangles = triFull;
|
|
3673
|
+
delete full.trianglesTruncated; delete full.trianglesLimit; delete full.trianglesTotal;
|
|
3674
|
+
return JSON.stringify(full, null, 2);
|
|
3675
|
+
};
|
|
3676
|
+
} else {
|
|
3677
|
+
// Fallback: read triangles from the face geometry
|
|
3678
|
+
const pos = target.geometry?.getAttribute?.('position');
|
|
3679
|
+
if (pos) {
|
|
3680
|
+
const v = new THREE.Vector3();
|
|
3681
|
+
const triCount = (pos.count / 3) | 0;
|
|
3682
|
+
const triFull = new Array(triCount);
|
|
3683
|
+
for (let i = 0; i < triCount; i++) {
|
|
3684
|
+
v.set(pos.getX(3 * i + 0), pos.getY(3 * i + 0), pos.getZ(3 * i + 0)).applyMatrix4(target.matrixWorld);
|
|
3685
|
+
const p0 = [this._round(v.x), this._round(v.y), this._round(v.z)];
|
|
3686
|
+
v.set(pos.getX(3 * i + 1), pos.getY(3 * i + 1), pos.getZ(3 * i + 1)).applyMatrix4(target.matrixWorld);
|
|
3687
|
+
const p1 = [this._round(v.x), this._round(v.y), this._round(v.z)];
|
|
3688
|
+
v.set(pos.getX(3 * i + 2), pos.getY(3 * i + 2), pos.getZ(3 * i + 2)).applyMatrix4(target.matrixWorld);
|
|
3689
|
+
const p2 = [this._round(v.x), this._round(v.y), this._round(v.z)];
|
|
3690
|
+
const ux = p1[0] - p0[0], uy = p1[1] - p0[1], uz = p1[2] - p0[2];
|
|
3691
|
+
const vx = p2[0] - p0[0], vy = p2[1] - p0[1], vz = p2[2] - p0[2];
|
|
3692
|
+
const cx = uy * vz - uz * vy, cy = uz * vx - ux * vz, cz = ux * vy - uy * vx; const len = Math.hypot(cx, cy, cz) || 1;
|
|
3693
|
+
triFull[i] = { p1: p0, p2: p1, p3: p2, normal: [this._round(cx / len), this._round(cy / len), this._round(cz / len)], area: this._round(0.5 * Math.hypot(cx, cy, cz)) };
|
|
3694
|
+
}
|
|
3695
|
+
try {
|
|
3696
|
+
let triMax = 5000; // preview cap for UI
|
|
3697
|
+
if (typeof window !== 'undefined' && Number.isFinite(window.BREP_DIAG_TRI_MAX_FACE)) triMax = window.BREP_DIAG_TRI_MAX_FACE | 0;
|
|
3698
|
+
if (triMax < 0) triMax = triFull.length;
|
|
3699
|
+
const count = Math.min(triFull.length, triMax);
|
|
3700
|
+
out.triangles = triFull.slice(0, count);
|
|
3701
|
+
if (count < triFull.length) { out.trianglesTruncated = true; out.trianglesTotal = triFull.length; out.trianglesLimit = triMax; }
|
|
3702
|
+
} catch { out.triangles = triFull; }
|
|
3703
|
+
downloadFactory = () => {
|
|
3704
|
+
const full = JSON.parse(JSON.stringify(out));
|
|
3705
|
+
full.triangles = triFull;
|
|
3706
|
+
delete full.trianglesTruncated; delete full.trianglesLimit; delete full.trianglesTotal;
|
|
3707
|
+
return JSON.stringify(full, null, 2);
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
} catch { }
|
|
3712
|
+
|
|
3713
|
+
// Edges connected to this face
|
|
3714
|
+
try {
|
|
3715
|
+
const edges = Array.isArray(target.edges) ? target.edges : [];
|
|
3716
|
+
out.edges = edges.map(e => ({ name: e.name || null, faces: (Array.isArray(e.faces) ? e.faces.map(f => f?.name || f?.userData?.faceName || null) : []), closedLoop: !!e.closedLoop, length: (typeof e.length === 'function' ? this._round(e.length()) : undefined), points: this._edgePointsWorld(e) }));
|
|
3717
|
+
} catch { out.edges = []; }
|
|
3718
|
+
|
|
3719
|
+
// Lazy-load unique vertices to improve performance
|
|
3720
|
+
try {
|
|
3721
|
+
out._lazyUniqueVertices = () => {
|
|
3722
|
+
const triangles = (out._lazyTriangles && typeof out._lazyTriangles === 'function') ? out._lazyTriangles() : [];
|
|
3723
|
+
const uniq = new Map();
|
|
3724
|
+
for (const tri of triangles) {
|
|
3725
|
+
for (const P of [tri.p1, tri.p2, tri.p3]) {
|
|
3726
|
+
const k = `${P[0]},${P[1]},${P[2]}`;
|
|
3727
|
+
if (!uniq.has(k)) uniq.set(k, P);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return Array.from(uniq.values());
|
|
3731
|
+
};
|
|
3732
|
+
} catch { }
|
|
3733
|
+
|
|
3734
|
+
// Basic metrics and orientation hints
|
|
3735
|
+
try { const n = target.getAverageNormal?.(); if (n) out.averageNormal = [this._round(n.x), this._round(n.y), this._round(n.z)]; } catch { }
|
|
3736
|
+
try {
|
|
3737
|
+
const a = target.surfaceArea?.();
|
|
3738
|
+
if (Number.isFinite(a)) {
|
|
3739
|
+
out.surfaceArea = this._round(a);
|
|
3740
|
+
// Make face area more prominent for easy reference
|
|
3741
|
+
out._faceAreaFormatted = `${this._round(a)} units²`;
|
|
3742
|
+
}
|
|
3743
|
+
} catch { }
|
|
3744
|
+
try {
|
|
3745
|
+
// Bounding box in world coords from triangle points (lazy-loaded)
|
|
3746
|
+
out._lazyBbox = () => {
|
|
3747
|
+
const pts = []; for (const tri of out.triangles || []) { pts.push(tri.p1, tri.p2, tri.p3); }
|
|
3748
|
+
if (pts.length) {
|
|
3749
|
+
let min = [+Infinity, +Infinity, +Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
3750
|
+
for (const p of pts) { if (p[0] < min[0]) min[0] = p[0]; if (p[1] < min[1]) min[1] = p[1]; if (p[2] < min[2]) min[2] = p[2]; if (p[0] > max[0]) max[0] = p[0]; if (p[1] > max[1]) max[1] = p[1]; if (p[2] > max[2]) max[2] = p[2]; }
|
|
3751
|
+
return { min, max };
|
|
3752
|
+
}
|
|
3753
|
+
return null;
|
|
3754
|
+
};
|
|
3755
|
+
} catch { }
|
|
3756
|
+
|
|
3757
|
+
// Neighbor face names
|
|
3758
|
+
try {
|
|
3759
|
+
const faceName = target?.name || target?.userData?.faceName || null;
|
|
3760
|
+
let neighbors = new Set();
|
|
3761
|
+
const solid = target?.parentSolid || target?.userData?.parentSolid || null;
|
|
3762
|
+
if (solid && typeof solid.getBoundaryEdgePolylines === 'function' && faceName) {
|
|
3763
|
+
const boundaries = solid.getBoundaryEdgePolylines() || [];
|
|
3764
|
+
for (const poly of boundaries) {
|
|
3765
|
+
const a = poly?.faceA;
|
|
3766
|
+
const b = poly?.faceB;
|
|
3767
|
+
if (a === faceName && b) neighbors.add(b);
|
|
3768
|
+
else if (b === faceName && a) neighbors.add(a);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
if (neighbors.size === 0 && solid && Array.isArray(solid.children)) {
|
|
3772
|
+
// Fallback: use the face's edges to gather neighbor faces in the current scene graph
|
|
3773
|
+
for (const edge of (target.edges || [])) {
|
|
3774
|
+
if (!edge || !Array.isArray(edge.faces)) continue;
|
|
3775
|
+
for (const f of edge.faces) {
|
|
3776
|
+
const n = f?.name || f?.userData?.faceName || null;
|
|
3777
|
+
if (n) neighbors.add(n);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
if (faceName) neighbors.delete(faceName);
|
|
3782
|
+
out.neighbors = Array.from(neighbors);
|
|
3783
|
+
} catch { }
|
|
3784
|
+
|
|
3785
|
+
// Boundary loops if available from metadata
|
|
3786
|
+
try {
|
|
3787
|
+
const loops = target.userData?.boundaryLoopsWorld;
|
|
3788
|
+
if (Array.isArray(loops) && loops.length) {
|
|
3789
|
+
out.boundaryLoops = loops.map(l => ({ isHole: !!l.isHole, pts: (Array.isArray(l.pts) ? l.pts : l).map(p => [this._round(p[0]), this._round(p[1]), this._round(p[2])]) }));
|
|
3790
|
+
}
|
|
3791
|
+
} catch { }
|
|
3792
|
+
} else if (target.type === 'EDGE') {
|
|
3793
|
+
out.closedLoop = !!target.closedLoop;
|
|
3794
|
+
// Lazy-load points to improve performance
|
|
3795
|
+
out._lazyPoints = () => this._edgePointsWorld(target);
|
|
3796
|
+
try {
|
|
3797
|
+
const len = target.length();
|
|
3798
|
+
if (Number.isFinite(len)) {
|
|
3799
|
+
out.length = this._round(len);
|
|
3800
|
+
out._edgeLengthFormatted = `${this._round(len)} units`;
|
|
3801
|
+
}
|
|
3802
|
+
} catch { }
|
|
3803
|
+
try { out.faces = (Array.isArray(target.faces) ? target.faces.map(f => f?.name || f?.userData?.faceName || null) : []); } catch { }
|
|
3804
|
+
} else if (target.type === 'SOLID') {
|
|
3805
|
+
try {
|
|
3806
|
+
const faces = target.getFaces?.(false) || [];
|
|
3807
|
+
out.faceCount = faces.length;
|
|
3808
|
+
out.faces = faces.slice(0, 10).map(f => ({ faceName: f.faceName, triangles: (f.triangles || []).length }));
|
|
3809
|
+
if (faces.length > 10) out.facesTruncated = true;
|
|
3810
|
+
} catch { }
|
|
3811
|
+
// Gather geometry arrays (prefer manifold mesh, fallback to authoring arrays)
|
|
3812
|
+
let arrays = null; let usedAuthoring = false;
|
|
3813
|
+
try {
|
|
3814
|
+
const mesh = target.getMesh?.();
|
|
3815
|
+
if (mesh && mesh.vertProperties && mesh.triVerts) {
|
|
3816
|
+
arrays = { vp: Array.from(mesh.vertProperties), tv: Array.from(mesh.triVerts), ids: Array.isArray(mesh.faceID) ? Array.from(mesh.faceID) : [] };
|
|
3817
|
+
}
|
|
3818
|
+
} catch { }
|
|
3819
|
+
if (!arrays) {
|
|
3820
|
+
try {
|
|
3821
|
+
const vp = Array.isArray(target._vertProperties) ? target._vertProperties.slice() : [];
|
|
3822
|
+
const tv = Array.isArray(target._triVerts) ? target._triVerts.slice() : [];
|
|
3823
|
+
const ids = Array.isArray(target._triIDs) ? target._triIDs.slice() : [];
|
|
3824
|
+
arrays = { vp, tv, ids }; usedAuthoring = true;
|
|
3825
|
+
} catch { }
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
if (arrays) {
|
|
3829
|
+
const { vp, tv, ids } = arrays;
|
|
3830
|
+
out.meshStats = { vertices: (vp.length / 3) | 0, triangles: (tv.length / 3) | 0, source: usedAuthoring ? 'authoring' : 'manifold' };
|
|
3831
|
+
// BBox
|
|
3832
|
+
let min = [+Infinity, +Infinity, +Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
3833
|
+
for (let i = 0; i < vp.length; i += 3) { const x = this._round(vp[i]), y = this._round(vp[i + 1]), z = this._round(vp[i + 2]); if (x < min[0]) min[0] = x; if (y < min[1]) min[1] = y; if (z < min[2]) min[2] = z; if (x > max[0]) max[0] = x; if (y > max[1]) max[1] = y; if (z > max[2]) max[2] = z; }
|
|
3834
|
+
if (min[0] !== Infinity) out.bbox = { min, max };
|
|
3835
|
+
|
|
3836
|
+
// Triangles with points (cap output size in preview; full list available via Download)
|
|
3837
|
+
try {
|
|
3838
|
+
const triCount = (tv.length / 3) | 0;
|
|
3839
|
+
let triMax = 5000; // sane default for UI
|
|
3840
|
+
try { if (typeof window !== 'undefined' && Number.isFinite(window.BREP_DIAG_TRI_MAX)) triMax = window.BREP_DIAG_TRI_MAX | 0; } catch { }
|
|
3841
|
+
if (triMax < 0) triMax = triCount; // -1 => no cap
|
|
3842
|
+
const count = Math.min(triCount, triMax);
|
|
3843
|
+
const tris = new Array(count);
|
|
3844
|
+
const nameOf = (id) => (target._idToFaceName && target._idToFaceName.get) ? target._idToFaceName.get(id) : undefined;
|
|
3845
|
+
for (let t = 0; t < count; t++) {
|
|
3846
|
+
const i0 = tv[3 * t + 0] >>> 0, i1 = tv[3 * t + 1] >>> 0, i2 = tv[3 * t + 2] >>> 0;
|
|
3847
|
+
const p0 = [this._round(vp[3 * i0 + 0]), this._round(vp[3 * i0 + 1]), this._round(vp[3 * i0 + 2])];
|
|
3848
|
+
const p1 = [this._round(vp[3 * i1 + 0]), this._round(vp[3 * i1 + 1]), this._round(vp[3 * i1 + 2])];
|
|
3849
|
+
const p2 = [this._round(vp[3 * i2 + 0]), this._round(vp[3 * i2 + 1]), this._round(vp[3 * i2 + 2])];
|
|
3850
|
+
let faceID = (Array.isArray(ids) && ids.length === triCount) ? ids[t] : undefined;
|
|
3851
|
+
const ux = p1[0] - p0[0], uy = p1[1] - p0[1], uz = p1[2] - p0[2];
|
|
3852
|
+
const vx = p2[0] - p0[0], vy = p2[1] - p0[1], vz = p2[2] - p0[2];
|
|
3853
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx; const nlen = Math.hypot(nx, ny, nz) || 1;
|
|
3854
|
+
tris[t] = {
|
|
3855
|
+
index: t,
|
|
3856
|
+
faceID: faceID,
|
|
3857
|
+
faceName: faceID !== undefined ? (nameOf(faceID) || null) : null,
|
|
3858
|
+
p1: p0, p2: p1, p3: p2,
|
|
3859
|
+
normal: [this._round(nx / nlen), this._round(ny / nlen), this._round(nz / nlen)],
|
|
3860
|
+
area: this._round(0.5 * nlen)
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
// Make triangles lazy-loaded for performance
|
|
3864
|
+
out._trianglesSummary = `${triCount} triangles (click to expand)`;
|
|
3865
|
+
out._lazyTriangles = () => tris;
|
|
3866
|
+
if (count < triCount) { out.trianglesTruncated = true; out.trianglesTotal = triCount; out.trianglesLimit = triMax; }
|
|
3867
|
+
// Build full JSON on demand
|
|
3868
|
+
downloadFactory = () => {
|
|
3869
|
+
const trisFull = new Array(triCount);
|
|
3870
|
+
const nameOf = (id) => (target._idToFaceName && target._idToFaceName.get) ? target._idToFaceName.get(id) : undefined;
|
|
3871
|
+
for (let t = 0; t < triCount; t++) {
|
|
3872
|
+
const i0 = tv[3 * t + 0] >>> 0, i1 = tv[3 * t + 1] >>> 0, i2 = tv[3 * t + 2] >>> 0;
|
|
3873
|
+
const p0 = [this._round(vp[3 * i0 + 0]), this._round(vp[3 * i0 + 1]), this._round(vp[3 * i0 + 2])];
|
|
3874
|
+
const p1 = [this._round(vp[3 * i1 + 0]), this._round(vp[3 * i1 + 1]), this._round(vp[3 * i1 + 2])];
|
|
3875
|
+
const p2 = [this._round(vp[3 * i2 + 0]), this._round(vp[3 * i2 + 1]), this._round(vp[3 * i2 + 2])];
|
|
3876
|
+
let faceID = (Array.isArray(ids) && ids.length === triCount) ? ids[t] : undefined;
|
|
3877
|
+
const ux = p1[0] - p0[0], uy = p1[1] - p0[1], uz = p1[2] - p0[2];
|
|
3878
|
+
const vx = p2[0] - p0[0], vy = p2[1] - p0[1], vz = p2[2] - p0[2];
|
|
3879
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx; const nlen = Math.hypot(nx, ny, nz) || 1;
|
|
3880
|
+
trisFull[t] = {
|
|
3881
|
+
index: t,
|
|
3882
|
+
faceID: faceID,
|
|
3883
|
+
faceName: faceID !== undefined ? (nameOf(faceID) || null) : null,
|
|
3884
|
+
p1: p0, p2: p1, p3: p2,
|
|
3885
|
+
normal: [this._round(nx / nlen), this._round(ny / nlen), this._round(nz / nlen)],
|
|
3886
|
+
area: this._round(0.5 * nlen)
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
const full = JSON.parse(JSON.stringify(out));
|
|
3890
|
+
full.triangles = trisFull; delete full.trianglesTruncated; delete full.trianglesLimit; delete full.trianglesTotal;
|
|
3891
|
+
return JSON.stringify(full, null, 2);
|
|
3892
|
+
};
|
|
3893
|
+
} catch { }
|
|
3894
|
+
|
|
3895
|
+
// Non-manifold / topology diagnostics (undirected edge uses)
|
|
3896
|
+
try {
|
|
3897
|
+
const nv = (vp.length / 3) | 0; const NV = BigInt(Math.max(1, nv));
|
|
3898
|
+
const eKey = (a, b) => { const A = BigInt(a), B = BigInt(b); return A < B ? A * NV + B : B * NV + A; };
|
|
3899
|
+
const e2c = new Map();
|
|
3900
|
+
const triCount = (tv.length / 3) | 0;
|
|
3901
|
+
const degenerate = []; const used = new Uint8Array(nv);
|
|
3902
|
+
for (let t = 0; t < triCount; t++) {
|
|
3903
|
+
const i0 = tv[3 * t + 0] >>> 0, i1 = tv[3 * t + 1] >>> 0, i2 = tv[3 * t + 2] >>> 0;
|
|
3904
|
+
used[i0] = 1; used[i1] = 1; used[i2] = 1;
|
|
3905
|
+
const ax = vp[3 * i0 + 0], ay = vp[3 * i0 + 1], az = vp[3 * i0 + 2];
|
|
3906
|
+
const bx = vp[3 * i1 + 0], by = vp[3 * i1 + 1], bz = vp[3 * i1 + 2];
|
|
3907
|
+
const cx = vp[3 * i2 + 0], cy = vp[3 * i2 + 1], cz = vp[3 * i2 + 2];
|
|
3908
|
+
const ux = bx - ax, uy = by - ay, uz = bz - az; const vx = cx - ax, vy = cy - ay, vz = cz - az;
|
|
3909
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx; const area2 = nx * nx + ny * ny + nz * nz;
|
|
3910
|
+
if (area2 <= 1e-30) degenerate.push(t);
|
|
3911
|
+
const add = (a, b) => { const k = eKey(Math.min(a, b), Math.max(a, b)); e2c.set(k, (e2c.get(k) || 0) + 1); };
|
|
3912
|
+
add(i0, i1); add(i1, i2); add(i2, i0);
|
|
3913
|
+
}
|
|
3914
|
+
let gt2 = 0, lt2 = 0, eq1 = 0; const exGT = [], exLT = [], exB = [];
|
|
3915
|
+
for (const [k, c] of e2c.entries()) {
|
|
3916
|
+
if (c > 2) { gt2++; if (exGT.length < 12) exGT.push({ edge: k.toString(), uses: c }); }
|
|
3917
|
+
else if (c < 2) { lt2++; if (c === 1) { eq1++; if (exB.length < 12) exB.push({ edge: k.toString(), uses: c }); } else { if (exLT.length < 12) exLT.push({ edge: k.toString(), uses: c }); } }
|
|
3918
|
+
}
|
|
3919
|
+
let isolated = 0; for (let i = 0; i < nv; i++) if (!used[i]) isolated++;
|
|
3920
|
+
const isClosed = (eq1 === 0);
|
|
3921
|
+
const hasNonManifoldEdges = (gt2 > 0);
|
|
3922
|
+
const isManifold = isClosed && !hasNonManifoldEdges;
|
|
3923
|
+
out.topology = {
|
|
3924
|
+
isManifold,
|
|
3925
|
+
closed: isClosed,
|
|
3926
|
+
nonManifoldEdges: hasNonManifoldEdges ? gt2 : 0,
|
|
3927
|
+
degenerateTriangles: { count: degenerate.length, examples: degenerate.slice(0, 12) },
|
|
3928
|
+
edges: { gt2, lt2, boundary: eq1, examples_gt2: exGT, examples_lt2: exLT, examples_boundary: exB },
|
|
3929
|
+
isolatedVertices: isolated
|
|
3930
|
+
};
|
|
3931
|
+
// Expose quick boolean at root for easy scanning
|
|
3932
|
+
out.isManifold = isManifold;
|
|
3933
|
+
} catch { }
|
|
3934
|
+
|
|
3935
|
+
// Faces fallback from authoring arrays when manifold faces unavailable
|
|
3936
|
+
if (!out.faceCount || !Array.isArray(out.faces)) {
|
|
3937
|
+
try {
|
|
3938
|
+
const nameOf = (id) => (target._idToFaceName && target._idToFaceName.get) ? target._idToFaceName.get(id) : String(id);
|
|
3939
|
+
const nameToTris = new Map();
|
|
3940
|
+
const triCount = (tv.length / 3) | 0;
|
|
3941
|
+
for (let t = 0; t < triCount; t++) {
|
|
3942
|
+
const id = Array.isArray(ids) ? ids[t] : undefined;
|
|
3943
|
+
const name = nameOf(id);
|
|
3944
|
+
if (!name) continue;
|
|
3945
|
+
let arr = nameToTris.get(name); if (!arr) { arr = []; nameToTris.set(name, arr); }
|
|
3946
|
+
arr.push(t);
|
|
3947
|
+
}
|
|
3948
|
+
const facesRaw = [];
|
|
3949
|
+
for (const [faceName, trisIdx] of nameToTris.entries()) facesRaw.push({ faceName, triangles: trisIdx.length });
|
|
3950
|
+
facesRaw.sort((a, b) => b.triangles - a.triangles);
|
|
3951
|
+
out.faceCount = facesRaw.length;
|
|
3952
|
+
out.faces = facesRaw.slice(0, 20);
|
|
3953
|
+
if (facesRaw.length > 20) out.facesTruncated = true;
|
|
3954
|
+
} catch { }
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
try { const vol = target.volume?.(); if (Number.isFinite(vol)) out.volume = this._round(vol); } catch { }
|
|
3959
|
+
try { const area = target.surfaceArea?.(); if (Number.isFinite(area)) out.surfaceArea = this._round(area); } catch { }
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
return { out, downloadFactory: downloadFactory || (() => JSON.stringify(out, null, 2)) };
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
_showDiagnosticsFor(target) {
|
|
3966
|
+
const { out, downloadFactory } = this._buildDiagnostics(target);
|
|
3967
|
+
const json = JSON.stringify(out, null, 2);
|
|
3968
|
+
this._showModal('Selection Diagnostics', json, { onDownload: downloadFactory });
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
_toast(msg, ms = 1200) {
|
|
3972
|
+
try {
|
|
3973
|
+
const el = document.createElement('div');
|
|
3974
|
+
el.textContent = msg;
|
|
3975
|
+
el.style.cssText = 'position:fixed;top:48px;left:50%;transform:translateX(-50%);background:#111c;backdrop-filter:blur(6px);color:#e5e7eb;padding:6px 10px;border:1px solid #2a3442;border-radius:8px;z-index:7;font:12px/1.2 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;';
|
|
3976
|
+
document.body.appendChild(el);
|
|
3977
|
+
setTimeout(() => { try { el.parentNode && el.parentNode.removeChild(el); } catch { } }, ms);
|
|
3978
|
+
} catch { }
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
_showModal(title, text, opts = {}) {
|
|
3982
|
+
const mask = document.createElement('div');
|
|
3983
|
+
mask.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(2px);z-index:7;display:flex;align-items:center;justify-content:center;';
|
|
3984
|
+
const box = document.createElement('div');
|
|
3985
|
+
box.style.cssText = 'width:min(980px,90vw);height:min(70vh,720px);background:#0b0d10;border:1px solid #2a3442;border-radius:10px;box-shadow:0 12px 28px rgba(0,0,0,.35);display:flex;flex-direction:column;overflow:hidden;';
|
|
3986
|
+
const header = document.createElement('div');
|
|
3987
|
+
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #1e2430;color:#e5e7eb;font:600 13px ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;';
|
|
3988
|
+
header.textContent = title || 'Diagnostics';
|
|
3989
|
+
const close = document.createElement('button');
|
|
3990
|
+
close.textContent = '✕';
|
|
3991
|
+
close.title = 'Close';
|
|
3992
|
+
close.style.cssText = 'margin-left:auto;background:transparent;border:0;color:#9aa4b2;cursor:pointer;font:700 14px ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;padding:4px;';
|
|
3993
|
+
const pre = document.createElement('textarea');
|
|
3994
|
+
pre.readOnly = true;
|
|
3995
|
+
pre.value = text || '';
|
|
3996
|
+
pre.style.cssText = 'flex:1;resize:none;background:#0f141a;color:#e5e7eb;border:0;padding:10px 12px;font:12px/1.3 ui-monospace,Menlo,Consolas,monospace;white-space:pre;';
|
|
3997
|
+
const foot = document.createElement('div');
|
|
3998
|
+
foot.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;padding:8px 12px;border-top:1px solid #1e2430;';
|
|
3999
|
+
const copyBtn = document.createElement('button');
|
|
4000
|
+
copyBtn.className = 'mtb-btn';
|
|
4001
|
+
copyBtn.textContent = 'Copy JSON';
|
|
4002
|
+
copyBtn.style.cssText = 'background:#1b2433;border:1px solid #334155;color:#e5e7eb;padding:6px 10px;border-radius:8px;cursor:pointer;font-weight:700;font-size:12px;';
|
|
4003
|
+
copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(pre.value); copyBtn.textContent = 'Copied!'; setTimeout(() => copyBtn.textContent = 'Copy JSON', 900); } catch { } });
|
|
4004
|
+
const dlBtn = document.createElement('button');
|
|
4005
|
+
dlBtn.className = 'mtb-btn';
|
|
4006
|
+
dlBtn.textContent = 'Download';
|
|
4007
|
+
dlBtn.style.cssText = copyBtn.style.cssText;
|
|
4008
|
+
dlBtn.addEventListener('click', () => {
|
|
4009
|
+
try {
|
|
4010
|
+
const content = (opts && typeof opts.onDownload === 'function') ? opts.onDownload() : pre.value;
|
|
4011
|
+
const blob = new Blob([content], { type: 'application/json' });
|
|
4012
|
+
const url = URL.createObjectURL(blob);
|
|
4013
|
+
const a = document.createElement('a');
|
|
4014
|
+
a.href = url; a.download = 'diagnostics.json'; document.body.appendChild(a); a.click();
|
|
4015
|
+
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
|
|
4016
|
+
} catch { }
|
|
4017
|
+
});
|
|
4018
|
+
|
|
4019
|
+
close.addEventListener('click', () => { try { document.body.removeChild(mask); } catch { } });
|
|
4020
|
+
mask.addEventListener('click', (e) => { if (e.target === mask) { try { document.body.removeChild(mask); } catch { } } });
|
|
4021
|
+
|
|
4022
|
+
header.appendChild(close);
|
|
4023
|
+
box.appendChild(header);
|
|
4024
|
+
box.appendChild(pre);
|
|
4025
|
+
foot.appendChild(copyBtn);
|
|
4026
|
+
foot.appendChild(dlBtn);
|
|
4027
|
+
box.appendChild(foot);
|
|
4028
|
+
mask.appendChild(box);
|
|
4029
|
+
document.body.appendChild(mask);
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
// ----------------------------------------
|
|
4033
|
+
// Internal: Resize & Camera Frustum
|
|
4034
|
+
// ----------------------------------------
|
|
4035
|
+
_getContainerSize() {
|
|
4036
|
+
// Prefer clientWidth/Height so we get the laid-out CSS size.
|
|
4037
|
+
// Fallback to window size if the container hasn't been laid out yet.
|
|
4038
|
+
const w = this.container.clientWidth || window.innerWidth || 1;
|
|
4039
|
+
const h = this.container.clientHeight || window.innerHeight || 1;
|
|
4040
|
+
return { width: Math.max(1, w), height: Math.max(1, h) };
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// REPLACE: _resizeRendererToDisplaySize()
|
|
4044
|
+
_resizeRendererToDisplaySize() {
|
|
4045
|
+
const { width, height } = this._getContainerSize();
|
|
4046
|
+
|
|
4047
|
+
const isWebGL = !!this.renderer?.isWebGLRenderer;
|
|
4048
|
+
let targetPR = 1;
|
|
4049
|
+
if (isWebGL && typeof this.renderer.getPixelRatio === 'function' && typeof this.renderer.setPixelRatio === 'function') {
|
|
4050
|
+
// Keep DPR current (handles moving across monitors)
|
|
4051
|
+
const dpr = window.devicePixelRatio || 1;
|
|
4052
|
+
targetPR = Math.max(1, Math.min(this.pixelRatio || dpr, dpr));
|
|
4053
|
+
if (this.renderer.getPixelRatio() !== targetPR) {
|
|
4054
|
+
this.renderer.setPixelRatio(targetPR);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
if (isWebGL) {
|
|
4059
|
+
// Ensure canvas CSS size matches container (use updateStyle=true)
|
|
4060
|
+
const canvas = this.renderer.domElement;
|
|
4061
|
+
const needResize =
|
|
4062
|
+
canvas.width !== Math.floor(width * targetPR) ||
|
|
4063
|
+
canvas.height !== Math.floor(height * targetPR);
|
|
4064
|
+
|
|
4065
|
+
if (needResize) {
|
|
4066
|
+
this.renderer.setSize(width, height, true);
|
|
4067
|
+
}
|
|
4068
|
+
} else if (this.renderer && typeof this.renderer.setSize === 'function') {
|
|
4069
|
+
this.renderer.setSize(width, height);
|
|
4070
|
+
try {
|
|
4071
|
+
const el = this.renderer.domElement;
|
|
4072
|
+
if (el) {
|
|
4073
|
+
el.style.width = '100%';
|
|
4074
|
+
el.style.height = '100%';
|
|
4075
|
+
}
|
|
4076
|
+
} catch { }
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
// Keep fat-line materials in sync with canvas resolution
|
|
4080
|
+
try {
|
|
4081
|
+
const setRes = (mat) => mat && mat.resolution && typeof mat.resolution.set === 'function' && mat.resolution.set(width, height);
|
|
4082
|
+
if (CADmaterials?.EDGE) {
|
|
4083
|
+
setRes(CADmaterials.EDGE.BASE);
|
|
4084
|
+
setRes(CADmaterials.EDGE.SELECTED);
|
|
4085
|
+
if (CADmaterials.EDGE.OVERLAY) setRes(CADmaterials.EDGE.OVERLAY);
|
|
4086
|
+
if (CADmaterials.EDGE.THREAD_SYMBOLIC_MAJOR) setRes(CADmaterials.EDGE.THREAD_SYMBOLIC_MAJOR);
|
|
4087
|
+
}
|
|
4088
|
+
if (CADmaterials?.LOOP) {
|
|
4089
|
+
setRes(CADmaterials.LOOP.BASE);
|
|
4090
|
+
setRes(CADmaterials.LOOP.SELECTED);
|
|
4091
|
+
}
|
|
4092
|
+
} catch { }
|
|
4093
|
+
// Ensure any per-object line materials stay in sync (metadata color clones, etc.)
|
|
4094
|
+
try {
|
|
4095
|
+
const scene = this.partHistory?.scene || this.scene;
|
|
4096
|
+
if (scene) {
|
|
4097
|
+
scene.traverse((obj) => {
|
|
4098
|
+
const mat = obj?.material;
|
|
4099
|
+
if (!mat) return;
|
|
4100
|
+
const apply = (m) => {
|
|
4101
|
+
if (m?.resolution && typeof m.resolution.set === 'function') {
|
|
4102
|
+
m.resolution.set(width, height);
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
if (Array.isArray(mat)) mat.forEach(apply);
|
|
4106
|
+
else apply(mat);
|
|
4107
|
+
});
|
|
4108
|
+
}
|
|
4109
|
+
} catch { }
|
|
4110
|
+
// Keep dashed overlays visually consistent in screen space
|
|
4111
|
+
this._updateOverlayDashSpacing(width, height);
|
|
4112
|
+
|
|
4113
|
+
// Update orthographic frustum for new aspect
|
|
4114
|
+
const aspect = width / height || 1;
|
|
4115
|
+
if (this.camera.isOrthographicCamera) {
|
|
4116
|
+
const spanYRaw = Number.isFinite(this.camera.top) && Number.isFinite(this.camera.bottom)
|
|
4117
|
+
? this.camera.top - this.camera.bottom
|
|
4118
|
+
: (this.viewSize * 2);
|
|
4119
|
+
const spanY = Math.abs(spanYRaw) > 1e-6 ? spanYRaw : (this.viewSize * 2);
|
|
4120
|
+
const centerY = (Number.isFinite(this.camera.top) && Number.isFinite(this.camera.bottom))
|
|
4121
|
+
? (this.camera.top + this.camera.bottom) * 0.5
|
|
4122
|
+
: 0;
|
|
4123
|
+
const centerX = (Number.isFinite(this.camera.left) && Number.isFinite(this.camera.right))
|
|
4124
|
+
? (this.camera.left + this.camera.right) * 0.5
|
|
4125
|
+
: 0;
|
|
4126
|
+
const halfHeight = Math.abs(spanY) * 0.5;
|
|
4127
|
+
const halfWidth = halfHeight * aspect;
|
|
4128
|
+
const signY = spanY >= 0 ? 1 : -1;
|
|
4129
|
+
this.camera.top = centerY + halfHeight * signY;
|
|
4130
|
+
this.camera.bottom = centerY - halfHeight * signY;
|
|
4131
|
+
this.camera.left = centerX - halfWidth;
|
|
4132
|
+
this.camera.right = centerX + halfWidth;
|
|
4133
|
+
} else {
|
|
4134
|
+
const v = this.viewSize;
|
|
4135
|
+
this.camera.left = -v * aspect;
|
|
4136
|
+
this.camera.right = v * aspect;
|
|
4137
|
+
this.camera.top = v;
|
|
4138
|
+
this.camera.bottom = -v;
|
|
4139
|
+
}
|
|
4140
|
+
this.camera.updateProjectionMatrix();
|
|
4141
|
+
|
|
4142
|
+
// Optional: let controls know something changed
|
|
4143
|
+
if (this.controls && typeof this.controls.update === 'function') {
|
|
4144
|
+
this.controls.update();
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// REPLACE: _onResize()
|
|
4149
|
+
_onResize() {
|
|
4150
|
+
// Coalesce rapid resize events to one rAF
|
|
4151
|
+
if (this._resizeScheduled) return;
|
|
4152
|
+
this._resizeScheduled = true;
|
|
4153
|
+
requestAnimationFrame(() => {
|
|
4154
|
+
this._resizeScheduled = false;
|
|
4155
|
+
this._resizeRendererToDisplaySize();
|
|
4156
|
+
this.render();
|
|
4157
|
+
// Keep overlayed labels/leaders in sync with new viewport
|
|
4158
|
+
try { this._sketchMode?.onCameraChanged?.(); } catch { }
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
// Re-evaluate hover while the camera animates/moves (e.g., orbiting)
|
|
4163
|
+
_onControlsChange() {
|
|
4164
|
+
if (this._disposed) return;
|
|
4165
|
+
// Re-evaluate hover while camera moves (if we have a last pointer)
|
|
4166
|
+
if (this._lastPointerEvent) this._updateHover(this._lastPointerEvent);
|
|
4167
|
+
// Keep dash lengths stable while zooming/panning/orbiting
|
|
4168
|
+
try {
|
|
4169
|
+
const size = this.renderer?.getSize?.(new THREE.Vector2()) || null;
|
|
4170
|
+
const w = size?.width || this.renderer?.domElement?.clientWidth || 0;
|
|
4171
|
+
const h = size?.height || this.renderer?.domElement?.clientHeight || 0;
|
|
4172
|
+
if (w && h) this._updateOverlayDashSpacing(w, h);
|
|
4173
|
+
} catch { }
|
|
4174
|
+
// While orbiting/panning/zooming, reposition dimension labels/leaders
|
|
4175
|
+
try { this._sketchMode?.onCameraChanged?.(); } catch { }
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
// Compute world-units per screen pixel for current camera and viewport
|
|
4179
|
+
_worldPerPixel(camera, width, height) {
|
|
4180
|
+
if (camera && camera.isOrthographicCamera) {
|
|
4181
|
+
const zoom = (typeof camera.zoom === 'number' && camera.zoom > 0) ? camera.zoom : 1;
|
|
4182
|
+
const wppX = (camera.right - camera.left) / (width * zoom);
|
|
4183
|
+
const wppY = (camera.top - camera.bottom) / (height * zoom);
|
|
4184
|
+
return Math.max(wppX, wppY);
|
|
4185
|
+
}
|
|
4186
|
+
const dist = camera.position.length();
|
|
4187
|
+
const fovRad = (camera.fov * Math.PI) / 180;
|
|
4188
|
+
return (2 * Math.tan(fovRad / 2) * dist) / height;
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
_updateOverlayDashSpacing(width, height) {
|
|
4192
|
+
if (!this.camera || !this.renderer) return;
|
|
4193
|
+
const w = width || this.renderer.domElement?.clientWidth || 0;
|
|
4194
|
+
const h = height || this.renderer.domElement?.clientHeight || 0;
|
|
4195
|
+
if (!w || !h) return;
|
|
4196
|
+
let wpp = null;
|
|
4197
|
+
try { wpp = this._worldPerPixel(this.camera, w, h); } catch { wpp = null; }
|
|
4198
|
+
if (!Number.isFinite(wpp) || wpp <= 0) return;
|
|
4199
|
+
if (this._lastDashWpp && Math.abs(this._lastDashWpp - wpp) < (this._lastDashWpp * 0.0005)) return;
|
|
4200
|
+
this._lastDashWpp = wpp;
|
|
4201
|
+
const dashPx = 10; // desired dash length in pixels
|
|
4202
|
+
const gapPx = 8; // desired gap length in pixels
|
|
4203
|
+
const setDash = (mat) => {
|
|
4204
|
+
if (!mat) return;
|
|
4205
|
+
try {
|
|
4206
|
+
mat.dashSize = dashPx * wpp;
|
|
4207
|
+
mat.gapSize = gapPx * wpp;
|
|
4208
|
+
mat.needsUpdate = true;
|
|
4209
|
+
} catch { }
|
|
4210
|
+
};
|
|
4211
|
+
try {
|
|
4212
|
+
const edges = CADmaterials?.EDGE || {};
|
|
4213
|
+
setDash(edges.OVERLAY);
|
|
4214
|
+
setDash(edges.THREAD_SYMBOLIC_MAJOR);
|
|
4215
|
+
} catch { }
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
|
|
4220
|
+
|
|
4221
|
+
window.DEBUG_MODE = false;
|
|
4222
|
+
|
|
4223
|
+
// function for debug logging that checks if we are in debug mode
|
|
4224
|
+
function debugLog(...args) {
|
|
4225
|
+
if (window.DEBUG_MODE) {
|
|
4226
|
+
console.log(...args);
|
|
4227
|
+
}
|
|
4228
|
+
}
|