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
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { CombinedTransformControls } from "../../UI/controls/CombinedTransformControls.js";
|
|
3
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RESOLUTION,
|
|
6
|
+
normalizeSplineData,
|
|
7
|
+
buildHermitePolyline,
|
|
8
|
+
cloneSplineData,
|
|
9
|
+
} from "./splineUtils.js";
|
|
10
|
+
|
|
11
|
+
const noop = () => { };
|
|
12
|
+
|
|
13
|
+
export class SplineEditorSession {
|
|
14
|
+
constructor(viewer, featureID, options = {}) {
|
|
15
|
+
this.viewer = viewer || null;
|
|
16
|
+
this.featureID =
|
|
17
|
+
featureID != null ? String(featureID) : options?.featureID || null;
|
|
18
|
+
this.options = options || {};
|
|
19
|
+
|
|
20
|
+
this._featureRef = options.featureRef || null;
|
|
21
|
+
this._previewResolution = DEFAULT_RESOLUTION;
|
|
22
|
+
this._splineData = normalizeSplineData(
|
|
23
|
+
cloneSplineData(this._featureRef?.persistentData?.spline || null)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
this._objectsById = new Map();
|
|
27
|
+
this._extensionLines = new Map();
|
|
28
|
+
this._selectedId = null;
|
|
29
|
+
this._hiddenArtifacts = [];
|
|
30
|
+
|
|
31
|
+
this._transformsById = new Map();
|
|
32
|
+
this._transformListeners = new Map();
|
|
33
|
+
this._isTransformDragging = false;
|
|
34
|
+
|
|
35
|
+
this._extensionLineMaterial = null;
|
|
36
|
+
|
|
37
|
+
this._previewGroup = null;
|
|
38
|
+
this._line = null;
|
|
39
|
+
|
|
40
|
+
this._onSplineChange =
|
|
41
|
+
typeof options.onSplineChange === "function"
|
|
42
|
+
? options.onSplineChange
|
|
43
|
+
: noop;
|
|
44
|
+
this._onSelectionChange =
|
|
45
|
+
typeof options.onSelectionChange === "function"
|
|
46
|
+
? options.onSelectionChange
|
|
47
|
+
: noop;
|
|
48
|
+
|
|
49
|
+
this._active = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_updateTransformVisibility() {
|
|
53
|
+
if (!this._transformsById) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if preview group exists, if not rebuild it
|
|
58
|
+
this._ensurePreviewGroup();
|
|
59
|
+
|
|
60
|
+
for (const [id, transformEntry] of this._transformsById.entries()) {
|
|
61
|
+
if (!transformEntry) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const { control } = transformEntry;
|
|
65
|
+
const active = id === this._selectedId;
|
|
66
|
+
|
|
67
|
+
if (control) {
|
|
68
|
+
control.enabled = active;
|
|
69
|
+
control.visible = active;
|
|
70
|
+
|
|
71
|
+
// Ensure proper scene management - remove from scene when inactive
|
|
72
|
+
if (this.viewer?.scene) {
|
|
73
|
+
if (active) {
|
|
74
|
+
// Add to scene if not already present
|
|
75
|
+
if (!this.viewer.scene.children.includes(control)) {
|
|
76
|
+
this.viewer.scene.add(control);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
// Remove from scene when inactive
|
|
80
|
+
if (this.viewer.scene.children.includes(control)) {
|
|
81
|
+
this.viewer.scene.remove(control);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!this._selectedId) {
|
|
88
|
+
this._isTransformDragging = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isActive() {
|
|
93
|
+
return this._active;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hasTransformControls() {
|
|
97
|
+
return this._transformsById && this._transformsById.size > 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getTransformControlCount() {
|
|
101
|
+
return this._transformsById ? this._transformsById.size : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getSplineData() {
|
|
105
|
+
return cloneSplineData(this._splineData);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getSelectedId() {
|
|
109
|
+
return this._selectedId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setFeatureRef(featureRef) {
|
|
113
|
+
this._featureRef = featureRef || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Activate the editing session. Builds preview geometry and attaches transform controls.
|
|
118
|
+
* @param {Object|null} initialSpline
|
|
119
|
+
* @param {Object} [options]
|
|
120
|
+
* @param {Object} [options.featureRef]
|
|
121
|
+
* @param {number} [options.previewResolution]
|
|
122
|
+
* @param {string} [options.initialSelection]
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
activate(initialSpline = null, options = {}) {
|
|
126
|
+
if (!this.viewer) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this._active) {
|
|
131
|
+
this.dispose();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const featureRef = options.featureRef ?? this._featureRef ?? null;
|
|
135
|
+
this._featureRef = featureRef;
|
|
136
|
+
|
|
137
|
+
const resCandidate =
|
|
138
|
+
options.previewResolution ??
|
|
139
|
+
Number(featureRef?.inputParams?.curveResolution);
|
|
140
|
+
if (Number.isFinite(resCandidate) && resCandidate >= 4) {
|
|
141
|
+
this._previewResolution = Math.max(4, Math.floor(resCandidate));
|
|
142
|
+
} else {
|
|
143
|
+
this._previewResolution = DEFAULT_RESOLUTION;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const source = initialSpline
|
|
147
|
+
? cloneSplineData(initialSpline)
|
|
148
|
+
: cloneSplineData(featureRef?.persistentData?.spline || null);
|
|
149
|
+
this._splineData = normalizeSplineData(source);
|
|
150
|
+
|
|
151
|
+
this._hideExistingArtifacts();
|
|
152
|
+
this._initMaterials();
|
|
153
|
+
this._buildPreviewGroup();
|
|
154
|
+
// Set up initial selection before rebuild
|
|
155
|
+
const initialSelection = options.initialSelection || null;
|
|
156
|
+
if (initialSelection) {
|
|
157
|
+
this._selectedId = initialSelection;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._rebuildAll({ preserveSelection: !!initialSelection });
|
|
161
|
+
|
|
162
|
+
this._active = true;
|
|
163
|
+
|
|
164
|
+
// Register with viewer to enable spline mode (suppress normal scene picking)
|
|
165
|
+
if (this.viewer && typeof this.viewer.startSplineMode === 'function') {
|
|
166
|
+
this.viewer.startSplineMode(this);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Hook into viewer's controls change event to update transform controls
|
|
170
|
+
this._setupControlsListener();
|
|
171
|
+
|
|
172
|
+
this._notifySelectionChange(this._selectedId);
|
|
173
|
+
this._renderOnce();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Tear down preview/controls and restore original artifacts.
|
|
179
|
+
*/
|
|
180
|
+
dispose() {
|
|
181
|
+
// Unregister from viewer to disable spline mode
|
|
182
|
+
if (this.viewer && typeof this.viewer.endSplineMode === 'function') {
|
|
183
|
+
this.viewer.endSplineMode();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove controls change listener
|
|
187
|
+
this._teardownControlsListener();
|
|
188
|
+
|
|
189
|
+
this._teardownAllTransforms();
|
|
190
|
+
this._destroyPreviewGroup();
|
|
191
|
+
this._restoreArtifacts();
|
|
192
|
+
this._disposeMaterials();
|
|
193
|
+
if (this._selectedId !== null) {
|
|
194
|
+
this._selectedId = null;
|
|
195
|
+
this._notifySelectionChange(null);
|
|
196
|
+
}
|
|
197
|
+
this._active = false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Update session spline data and rebuild preview.
|
|
202
|
+
* @param {Object} spline
|
|
203
|
+
* @param {Object} [options]
|
|
204
|
+
* @param {boolean} [options.preserveSelection=true]
|
|
205
|
+
* @param {boolean} [options.silent=false]
|
|
206
|
+
* @param {string} [options.reason="manual"]
|
|
207
|
+
*/
|
|
208
|
+
setSplineData(spline, options = {}) {
|
|
209
|
+
|
|
210
|
+
const {
|
|
211
|
+
preserveSelection = true,
|
|
212
|
+
silent = false,
|
|
213
|
+
reason = "manual",
|
|
214
|
+
} = options;
|
|
215
|
+
|
|
216
|
+
const normalized = normalizeSplineData(cloneSplineData(spline));
|
|
217
|
+
this._splineData = normalized;
|
|
218
|
+
|
|
219
|
+
// Update the feature's persistent data immediately
|
|
220
|
+
this._updateFeaturePersistentData();
|
|
221
|
+
|
|
222
|
+
// CRITICAL FIX: Don't rebuild everything if this is just a transform update
|
|
223
|
+
if (reason === "transform" && preserveSelection) {
|
|
224
|
+
// Just update the preview line, don't rebuild point handles (which destroys transforms)
|
|
225
|
+
this._rebuildPreviewLine();
|
|
226
|
+
this._updateExtensionLinesForAllPoints();
|
|
227
|
+
} else {
|
|
228
|
+
this._rebuildAll({ preserveSelection });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!silent) {
|
|
232
|
+
this._notifySplineChange(reason);
|
|
233
|
+
} else {
|
|
234
|
+
this._renderOnce();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
selectObject(id, options = {}) {
|
|
239
|
+
const { silent = false, forceRedraw = false } = options || {};
|
|
240
|
+
const nextId = id == null ? null : String(id);
|
|
241
|
+
|
|
242
|
+
if (this._selectedId === nextId && !forceRedraw) {
|
|
243
|
+
if (!silent) {
|
|
244
|
+
this._notifySelectionChange(this._selectedId);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Ensure preview group exists before changing selection
|
|
250
|
+
this._ensurePreviewGroup();
|
|
251
|
+
|
|
252
|
+
this._selectedId = nextId;
|
|
253
|
+
|
|
254
|
+
// If forcing redraw, rebuild everything to ensure fresh state
|
|
255
|
+
if (forceRedraw) {
|
|
256
|
+
this._rebuildAll({ preserveSelection: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this._updateSelectionVisuals();
|
|
260
|
+
|
|
261
|
+
this._updateTransformVisibility();
|
|
262
|
+
|
|
263
|
+
if (!silent) {
|
|
264
|
+
this._notifySelectionChange(this._selectedId);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this._renderOnce();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
clearSelection() {
|
|
271
|
+
this.selectObject(null);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
hideGizmo() {
|
|
275
|
+
this.clearSelection();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Force cleanup of any stale objects in the scene
|
|
280
|
+
*/
|
|
281
|
+
forceCleanup() {
|
|
282
|
+
|
|
283
|
+
if (!this.viewer?.scene) return;
|
|
284
|
+
|
|
285
|
+
// Find and remove any stale transform controls
|
|
286
|
+
const toRemove = [];
|
|
287
|
+
this.viewer.scene.traverse((obj) => {
|
|
288
|
+
// Look for transform controls that might be stale
|
|
289
|
+
if (obj.type === 'CombinedTransformControls' || obj.isTransformGizmo) {
|
|
290
|
+
// Check if this control is in our current transforms map
|
|
291
|
+
let isValid = false;
|
|
292
|
+
if (this._transformsById) {
|
|
293
|
+
for (const entry of this._transformsById.values()) {
|
|
294
|
+
if (entry.control === obj) {
|
|
295
|
+
isValid = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!isValid) {
|
|
301
|
+
toRemove.push(obj);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Remove stale objects
|
|
307
|
+
for (const obj of toRemove) {
|
|
308
|
+
try {
|
|
309
|
+
this.viewer.scene.remove(obj);
|
|
310
|
+
obj.dispose?.();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
/* ignore */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this._renderOnce();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_renderOnce() {
|
|
320
|
+
try {
|
|
321
|
+
this.viewer?.render?.();
|
|
322
|
+
} catch {
|
|
323
|
+
/* noop */
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_setupControlsListener() {
|
|
328
|
+
// Listen to camera/controls changes to update transform controls screen size
|
|
329
|
+
this._controlsChangeHandler = () => {
|
|
330
|
+
if (this._transformsById) {
|
|
331
|
+
for (const [id, transformEntry] of this._transformsById.entries()) {
|
|
332
|
+
const control = transformEntry?.control;
|
|
333
|
+
if (control && typeof control.update === 'function') {
|
|
334
|
+
try {
|
|
335
|
+
control.update();
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn(`SplineEditorSession: Failed to update transform control ${id}:`, error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Hook into the viewer's controls change event
|
|
345
|
+
if (this.viewer?.controls && typeof this.viewer.controls.addEventListener === 'function') {
|
|
346
|
+
this.viewer.controls.addEventListener('change', this._controlsChangeHandler);
|
|
347
|
+
this.viewer.controls.addEventListener('end', this._controlsChangeHandler);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_teardownControlsListener() {
|
|
352
|
+
if (this._controlsChangeHandler && this.viewer?.controls) {
|
|
353
|
+
try {
|
|
354
|
+
this.viewer.controls.removeEventListener('change', this._controlsChangeHandler);
|
|
355
|
+
this.viewer.controls.removeEventListener('end', this._controlsChangeHandler);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.warn('SplineEditorSession: Failed to remove controls listeners:', error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this._controlsChangeHandler = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_notifySplineChange(reason, extra = null) {
|
|
364
|
+
try {
|
|
365
|
+
this._onSplineChange(this.getSplineData(), reason, extra);
|
|
366
|
+
} catch {
|
|
367
|
+
/* ignore listener errors */
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_updateFeaturePersistentData() {
|
|
372
|
+
// Update the feature's persistent data immediately
|
|
373
|
+
if (this._featureRef) {
|
|
374
|
+
this._featureRef.persistentData = this._featureRef.persistentData || {};
|
|
375
|
+
this._featureRef.persistentData.spline = cloneSplineData(this._splineData);
|
|
376
|
+
|
|
377
|
+
// Mark the feature as dirty for rebuild
|
|
378
|
+
this._featureRef.lastRunInputParams = {};
|
|
379
|
+
this._featureRef.timestamp = 0;
|
|
380
|
+
this._featureRef.dirty = true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_notifySelectionChange(id) {
|
|
385
|
+
try {
|
|
386
|
+
this._onSelectionChange(id);
|
|
387
|
+
} catch {
|
|
388
|
+
/* ignore listener errors */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_hideExistingArtifacts() {
|
|
393
|
+
const scene = this.viewer?.scene;
|
|
394
|
+
if (!scene) return;
|
|
395
|
+
this._hiddenArtifacts = [];
|
|
396
|
+
scene.traverse((obj) => {
|
|
397
|
+
if (obj && obj.owningFeatureID === this.featureID && obj.visible) {
|
|
398
|
+
this._hiddenArtifacts.push({ obj, visible: obj.visible });
|
|
399
|
+
obj.visible = false;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_restoreArtifacts() {
|
|
405
|
+
for (const entry of this._hiddenArtifacts) {
|
|
406
|
+
try {
|
|
407
|
+
entry.obj.visible = entry.visible;
|
|
408
|
+
} catch {
|
|
409
|
+
/* ignore */
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
this._hiddenArtifacts = [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_initMaterials() {
|
|
416
|
+
// Extension line material - thicker than the main spline
|
|
417
|
+
this._extensionLineMaterial = new THREE.LineBasicMaterial({
|
|
418
|
+
color: "blue",
|
|
419
|
+
linewidth: 3, // Thicker than the main spline
|
|
420
|
+
transparent: true,
|
|
421
|
+
opacity: 0.85,
|
|
422
|
+
depthTest: false,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
_disposeMaterials() {
|
|
427
|
+
try {
|
|
428
|
+
this._extensionLineMaterial?.dispose?.();
|
|
429
|
+
} catch {
|
|
430
|
+
/* noop */
|
|
431
|
+
}
|
|
432
|
+
this._extensionLineMaterial = null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_buildPreviewGroup() {
|
|
436
|
+
const scene = this.viewer?.scene;
|
|
437
|
+
if (!scene) return;
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// remove the actual spline from the scene. The spline generated by the feature it self, not the preview
|
|
442
|
+
// The object to be removed from the scene will have the same name as the feature ID
|
|
443
|
+
const existingSpline = scene.getObjectByName(this.featureID);
|
|
444
|
+
if (existingSpline) {
|
|
445
|
+
scene.remove(existingSpline);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Search the scene for an existing preview group and reuse it rather than creating a new one
|
|
449
|
+
const existingGroupName = `SplineEditorPreview:${this.featureID || ""}`;
|
|
450
|
+
const existingGroup = scene.getObjectByName(existingGroupName);
|
|
451
|
+
if (existingGroup) {
|
|
452
|
+
this._previewGroup = existingGroup;
|
|
453
|
+
// remove all children from existing group
|
|
454
|
+
while (this._previewGroup.children.length > 0) {
|
|
455
|
+
this._previewGroup.remove(this._previewGroup.children[0]);
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
this._previewGroup = new THREE.Group();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
this._previewGroup.name = `SplineEditorPreview:${this.featureID || ""}`;
|
|
463
|
+
this._previewGroup.userData = this._previewGroup.userData || {};
|
|
464
|
+
this._previewGroup.userData.excludeFromFit = true;
|
|
465
|
+
this._previewGroup.userData.preventRemove = true;
|
|
466
|
+
|
|
467
|
+
const lineMaterial = new THREE.LineBasicMaterial({
|
|
468
|
+
color: 0xffffff,
|
|
469
|
+
linewidth: 2,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const geometry = new THREE.BufferGeometry();
|
|
473
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute([], 3));
|
|
474
|
+
|
|
475
|
+
this._line = new THREE.Line(geometry, lineMaterial);
|
|
476
|
+
this._line.userData = this._line.userData || {};
|
|
477
|
+
this._line.userData.excludeFromFit = true;
|
|
478
|
+
this._line.renderOrder = 10000;
|
|
479
|
+
this._previewGroup.add(this._line);
|
|
480
|
+
|
|
481
|
+
scene.add(this._previewGroup);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_ensurePreviewGroup() {
|
|
485
|
+
const scene = this.viewer.scene;
|
|
486
|
+
|
|
487
|
+
// Check if the preview group still exists in the scene
|
|
488
|
+
if (!this._previewGroup || !this.viewer?.scene) {
|
|
489
|
+
this._buildPreviewGroup();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check if the preview group was removed from the scene
|
|
494
|
+
|
|
495
|
+
const existingGroupName = `SplineEditorPreview:${this.featureID || ""}`;
|
|
496
|
+
const foundInScene = scene.getObjectByName(existingGroupName);
|
|
497
|
+
|
|
498
|
+
if (!foundInScene) {
|
|
499
|
+
// Preview group was removed, sync with latest persistent data and rebuild
|
|
500
|
+
const latestSplineData = this._featureRef?.persistentData?.spline;
|
|
501
|
+
if (latestSplineData) {
|
|
502
|
+
this._splineData = normalizeSplineData(cloneSplineData(latestSplineData));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Update preview resolution from current feature parameters
|
|
506
|
+
const resCandidate = Number(this._featureRef?.inputParams?.curveResolution);
|
|
507
|
+
if (Number.isFinite(resCandidate) && resCandidate >= 4) {
|
|
508
|
+
this._previewResolution = Math.max(4, Math.floor(resCandidate));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this._buildPreviewGroup();
|
|
512
|
+
this._rebuildAll({ preserveSelection: true });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_destroyPreviewGroup() {
|
|
517
|
+
if (!this._previewGroup || !this.viewer?.scene) {
|
|
518
|
+
this._teardownAllTransforms();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
this._teardownAllTransforms();
|
|
522
|
+
this._removeExtensionLines();
|
|
523
|
+
try {
|
|
524
|
+
if (this._line) {
|
|
525
|
+
this._line.geometry?.dispose();
|
|
526
|
+
this._line.material?.dispose();
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
/* noop */
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
// Clear the preventRemove flag before removing from scene
|
|
533
|
+
if (this._previewGroup.userData) {
|
|
534
|
+
this._previewGroup.userData.preventRemove = false;
|
|
535
|
+
}
|
|
536
|
+
this.viewer.scene.remove(this._previewGroup);
|
|
537
|
+
} catch {
|
|
538
|
+
/* noop */
|
|
539
|
+
}
|
|
540
|
+
this._previewGroup = null;
|
|
541
|
+
this._line = null;
|
|
542
|
+
this._objectsById.clear();
|
|
543
|
+
this._extensionLines.clear();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
_createTransformControl(id, mesh) {
|
|
547
|
+
if (!this.viewer?.scene || !this.viewer?.camera || !this.viewer?.renderer) {
|
|
548
|
+
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
if (!CombinedTransformControls) {
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const control = new CombinedTransformControls(
|
|
556
|
+
this.viewer.camera,
|
|
557
|
+
this.viewer.renderer.domElement
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
control.name = `SplineEditorControl:${id}`;
|
|
561
|
+
|
|
562
|
+
control.setMode("translate");
|
|
563
|
+
control.showX = true;
|
|
564
|
+
control.showY = true;
|
|
565
|
+
control.showZ = true;
|
|
566
|
+
control.enabled = false; // Will be enabled when selected
|
|
567
|
+
control.attach(mesh);
|
|
568
|
+
control.userData = control.userData || {};
|
|
569
|
+
control.userData.excludeFromFit = true;
|
|
570
|
+
|
|
571
|
+
// Add the transform control directly to the scene
|
|
572
|
+
this.viewer.scene.add(control);
|
|
573
|
+
|
|
574
|
+
const changeHandler = () => this._handleTransformChangeFor(id);
|
|
575
|
+
const dragHandler = (event) => {
|
|
576
|
+
this._handleTransformDragging(!!event?.value);
|
|
577
|
+
};
|
|
578
|
+
control.addEventListener("change", changeHandler);
|
|
579
|
+
control.addEventListener("dragging-changed", dragHandler);
|
|
580
|
+
|
|
581
|
+
this._transformsById.set(id, { control });
|
|
582
|
+
this._transformListeners.set(id, { changeHandler, dragHandler });
|
|
583
|
+
return control;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_teardownAllTransforms() {
|
|
587
|
+
if (!this._transformsById?.size) {
|
|
588
|
+
this._isTransformDragging = false;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const [id, transformEntry] of this._transformsById.entries()) {
|
|
593
|
+
|
|
594
|
+
const control = transformEntry?.control || null;
|
|
595
|
+
const listeners = this._transformListeners.get(id);
|
|
596
|
+
|
|
597
|
+
if (control && listeners) {
|
|
598
|
+
try {
|
|
599
|
+
control.removeEventListener("change", listeners.changeHandler);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
/* ignore */
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
control.removeEventListener("dragging-changed", listeners.dragHandler);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
/* ignore */
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
control?.detach?.();
|
|
612
|
+
} catch (error) {
|
|
613
|
+
/* ignore */
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Remove control from scene
|
|
617
|
+
if (control) {
|
|
618
|
+
try {
|
|
619
|
+
this.viewer?.scene?.remove(control);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
/* ignore */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
control?.dispose?.();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
/* ignore */
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
this._transformsById.clear();
|
|
632
|
+
this._transformListeners.clear();
|
|
633
|
+
this._isTransformDragging = false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
_rebuildAll({ preserveSelection }) {
|
|
637
|
+
|
|
638
|
+
// Force cleanup of any stale objects before rebuild
|
|
639
|
+
this.forceCleanup();
|
|
640
|
+
|
|
641
|
+
const previousSelection = preserveSelection ? this._selectedId : null;
|
|
642
|
+
|
|
643
|
+
this._buildPointHandles();
|
|
644
|
+
|
|
645
|
+
this._rebuildPreviewLine();
|
|
646
|
+
|
|
647
|
+
if (preserveSelection && previousSelection) {
|
|
648
|
+
this.selectObject(previousSelection, { silent: true });
|
|
649
|
+
} else if (!preserveSelection) {
|
|
650
|
+
this.selectObject(null, { silent: true });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
_buildPointHandles() {
|
|
656
|
+
|
|
657
|
+
if (!this._previewGroup) return;
|
|
658
|
+
|
|
659
|
+
this._teardownAllTransforms();
|
|
660
|
+
|
|
661
|
+
// Clear EVERYTHING from the preview group - no preservation
|
|
662
|
+
while (this._previewGroup.children.length > 0) {
|
|
663
|
+
const child = this._previewGroup.children[0];
|
|
664
|
+
this._previewGroup.remove(child);
|
|
665
|
+
try {
|
|
666
|
+
child.geometry?.dispose();
|
|
667
|
+
child.material?.dispose();
|
|
668
|
+
} catch {
|
|
669
|
+
/* noop */
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Clear all tracking maps completely
|
|
674
|
+
this._objectsById.clear();
|
|
675
|
+
this._extensionLines.clear();
|
|
676
|
+
|
|
677
|
+
this._initMaterials();
|
|
678
|
+
|
|
679
|
+
// Create fresh spline line
|
|
680
|
+
const lineMaterial = new THREE.LineBasicMaterial({
|
|
681
|
+
color: 0xffffff,
|
|
682
|
+
linewidth: 2,
|
|
683
|
+
});
|
|
684
|
+
const geometry = new THREE.BufferGeometry();
|
|
685
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute([], 3));
|
|
686
|
+
this._line = new THREE.Line(geometry, lineMaterial);
|
|
687
|
+
this._line.name = "SplinePreviewLine";
|
|
688
|
+
this._previewGroup.add(this._line);
|
|
689
|
+
|
|
690
|
+
// Create handles for each point - no separate extension handles
|
|
691
|
+
this._splineData.points.forEach((pt, index) => {
|
|
692
|
+
// Create a simple point geometry for invisible click target
|
|
693
|
+
const pointGeometry = new THREE.BufferGeometry();
|
|
694
|
+
const position = new Float32Array([
|
|
695
|
+
Number(pt.position[0]) || 0,
|
|
696
|
+
Number(pt.position[1]) || 0,
|
|
697
|
+
Number(pt.position[2]) || 0
|
|
698
|
+
]);
|
|
699
|
+
pointGeometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
|
|
700
|
+
|
|
701
|
+
// Create an invisible mesh for raycasting and transform attachment
|
|
702
|
+
const pointMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
703
|
+
const mesh = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), pointMaterial);
|
|
704
|
+
mesh.position.set(position[0], position[1], position[2]);
|
|
705
|
+
|
|
706
|
+
// Apply stored rotation to the mesh
|
|
707
|
+
if (pt.rotation && Array.isArray(pt.rotation) && pt.rotation.length === 9) {
|
|
708
|
+
const rotMatrix = new THREE.Matrix3().fromArray(pt.rotation);
|
|
709
|
+
const matrix4 = new THREE.Matrix4().setFromMatrix3(rotMatrix);
|
|
710
|
+
mesh.setRotationFromMatrix(matrix4);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
mesh.name = `SplinePoint:${pt.id}`;
|
|
714
|
+
this._previewGroup.add(mesh);
|
|
715
|
+
|
|
716
|
+
// Create a clickable vertex at the same position using BREP.Vertex
|
|
717
|
+
const vertex = new BREP.Vertex([position[0], position[1], position[2]], {
|
|
718
|
+
name: `SplineVertex:${pt.id}`,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Add click handler to the vertex to trigger selection
|
|
722
|
+
vertex.onClick = () => {
|
|
723
|
+
this.selectObject(`point:${pt.id}`);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// Store reference to the point for identification - set on both vertex and internal point
|
|
727
|
+
vertex.userData = vertex.userData || {};
|
|
728
|
+
vertex.userData.splineFeatureId = this.featureID;
|
|
729
|
+
vertex.userData.splinePointId = pt.id;
|
|
730
|
+
vertex.userData.isSplineVertex = true;
|
|
731
|
+
|
|
732
|
+
// Also set userData on the internal Points object that gets hit by raycaster
|
|
733
|
+
if (vertex._point) {
|
|
734
|
+
vertex._point.userData = vertex._point.userData || {};
|
|
735
|
+
vertex._point.userData.splineFeatureId = this.featureID;
|
|
736
|
+
vertex._point.userData.splinePointId = pt.id;
|
|
737
|
+
vertex._point.userData.isSplineVertex = true;
|
|
738
|
+
// Copy the onClick handler to the internal point
|
|
739
|
+
vertex._point.onClick = vertex.onClick;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Add vertex to preview group
|
|
743
|
+
this._previewGroup.add(vertex);
|
|
744
|
+
|
|
745
|
+
const entryId = `point:${pt.id}`;
|
|
746
|
+
const transform = this._createTransformControl(entryId, mesh);
|
|
747
|
+
this._objectsById.set(entryId, {
|
|
748
|
+
type: "point",
|
|
749
|
+
mesh,
|
|
750
|
+
vertex, // Store reference to the clickable vertex
|
|
751
|
+
data: pt,
|
|
752
|
+
transform,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Create extension lines for this point
|
|
756
|
+
const forwardKey = `forward-line:${pt.id}`;
|
|
757
|
+
const backwardKey = `backward-line:${pt.id}`;
|
|
758
|
+
|
|
759
|
+
// Forward extension line
|
|
760
|
+
const forwardGeometry = new THREE.BufferGeometry();
|
|
761
|
+
const forwardLine = new THREE.Line(forwardGeometry, this._extensionLineMaterial);
|
|
762
|
+
forwardLine.name = `SplineForwardLine:${pt.id}`;
|
|
763
|
+
forwardLine.visible = true;
|
|
764
|
+
forwardLine.frustumCulled = false;
|
|
765
|
+
this._previewGroup.add(forwardLine);
|
|
766
|
+
this._extensionLines.set(forwardKey, forwardLine);
|
|
767
|
+
|
|
768
|
+
// Backward extension line
|
|
769
|
+
const backwardGeometry = new THREE.BufferGeometry();
|
|
770
|
+
const backwardLine = new THREE.Line(backwardGeometry, this._extensionLineMaterial);
|
|
771
|
+
backwardLine.name = `SplineBackwardLine:${pt.id}`;
|
|
772
|
+
backwardLine.visible = true;
|
|
773
|
+
backwardLine.frustumCulled = false;
|
|
774
|
+
this._previewGroup.add(backwardLine);
|
|
775
|
+
this._extensionLines.set(backwardKey, backwardLine);
|
|
776
|
+
|
|
777
|
+
// Set the geometry for the extension lines
|
|
778
|
+
const rotation = pt.rotation || [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
779
|
+
let xAxisDirection = [rotation[0], rotation[1], rotation[2]];
|
|
780
|
+
|
|
781
|
+
const length = Math.sqrt(xAxisDirection[0] * xAxisDirection[0] + xAxisDirection[1] * xAxisDirection[1] + xAxisDirection[2] * xAxisDirection[2]);
|
|
782
|
+
if (length > 0) {
|
|
783
|
+
xAxisDirection = [xAxisDirection[0] / length, xAxisDirection[1] / length, xAxisDirection[2] / length];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const forwardDir = pt.flipDirection ? [-xAxisDirection[0], -xAxisDirection[1], -xAxisDirection[2]] : xAxisDirection;
|
|
787
|
+
const backwardDir = pt.flipDirection ? xAxisDirection : [-xAxisDirection[0], -xAxisDirection[1], -xAxisDirection[2]];
|
|
788
|
+
|
|
789
|
+
// Forward line geometry
|
|
790
|
+
if (pt.forwardDistance > 0) {
|
|
791
|
+
const forwardEnd = [
|
|
792
|
+
pt.position[0] + forwardDir[0] * pt.forwardDistance,
|
|
793
|
+
pt.position[1] + forwardDir[1] * pt.forwardDistance,
|
|
794
|
+
pt.position[2] + forwardDir[2] * pt.forwardDistance
|
|
795
|
+
];
|
|
796
|
+
forwardGeometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
797
|
+
pt.position[0], pt.position[1], pt.position[2],
|
|
798
|
+
forwardEnd[0], forwardEnd[1], forwardEnd[2]
|
|
799
|
+
], 3));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Backward line geometry
|
|
803
|
+
if (pt.backwardDistance > 0) {
|
|
804
|
+
const backwardEnd = [
|
|
805
|
+
pt.position[0] + backwardDir[0] * pt.backwardDistance,
|
|
806
|
+
pt.position[1] + backwardDir[1] * pt.backwardDistance,
|
|
807
|
+
pt.position[2] + backwardDir[2] * pt.backwardDistance
|
|
808
|
+
];
|
|
809
|
+
backwardGeometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
810
|
+
pt.position[0], pt.position[1], pt.position[2],
|
|
811
|
+
backwardEnd[0], backwardEnd[1], backwardEnd[2]
|
|
812
|
+
], 3));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
this._updateTransformVisibility();
|
|
817
|
+
|
|
818
|
+
// Rebuild the spline curve as well
|
|
819
|
+
this._rebuildPreviewLine();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_rebuildPreviewLine() {
|
|
823
|
+
if (!this._line) return;
|
|
824
|
+
|
|
825
|
+
// Clean up old geometry to prevent memory leaks
|
|
826
|
+
const oldGeometry = this._line.geometry;
|
|
827
|
+
if (oldGeometry) {
|
|
828
|
+
// Clear old attributes
|
|
829
|
+
const positionAttr = oldGeometry.getAttribute("position");
|
|
830
|
+
if (positionAttr) {
|
|
831
|
+
positionAttr.needsUpdate = true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const bendRadius = Number.isFinite(Number(this._featureRef?.inputParams?.bendRadius))
|
|
836
|
+
? Math.max(0.1, Math.min(5.0, Number(this._featureRef.inputParams.bendRadius)))
|
|
837
|
+
: 1.0;
|
|
838
|
+
|
|
839
|
+
const { positions } = buildHermitePolyline(
|
|
840
|
+
this._splineData,
|
|
841
|
+
this._previewResolution || DEFAULT_RESOLUTION,
|
|
842
|
+
bendRadius
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const array = new Float32Array(positions);
|
|
846
|
+
this._line.geometry.setAttribute(
|
|
847
|
+
"position",
|
|
848
|
+
new THREE.BufferAttribute(array, 3)
|
|
849
|
+
);
|
|
850
|
+
if (positions.length >= 3) {
|
|
851
|
+
this._line.geometry.computeBoundingSphere();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
_updateSelectionVisuals() {
|
|
857
|
+
// Update vertex selection states to show which vertex is selected
|
|
858
|
+
for (const [id, entry] of this._objectsById.entries()) {
|
|
859
|
+
if (entry.vertex) {
|
|
860
|
+
const isSelected = id === this._selectedId;
|
|
861
|
+
entry.vertex.selected = isSelected;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
_handleTransformChangeFor(id = null) {
|
|
867
|
+
|
|
868
|
+
const targetId =
|
|
869
|
+
id && this._objectsById.has(id) ? id : this._selectedId;
|
|
870
|
+
if (!targetId) return;
|
|
871
|
+
const entry = this._objectsById.get(targetId);
|
|
872
|
+
if (!entry || !entry.mesh || !entry.data) return;
|
|
873
|
+
|
|
874
|
+
const pos = entry.mesh.position;
|
|
875
|
+
if (entry.type === "point") {
|
|
876
|
+
// Update point position
|
|
877
|
+
entry.data.position = [pos.x, pos.y, pos.z];
|
|
878
|
+
|
|
879
|
+
// Update point rotation - extract rotation matrix from mesh
|
|
880
|
+
const rotMatrix = new THREE.Matrix3().setFromMatrix4(entry.mesh.matrix);
|
|
881
|
+
entry.data.rotation = rotMatrix.elements.slice(); // Store as flat array
|
|
882
|
+
|
|
883
|
+
// Update the vertex position to match the mesh
|
|
884
|
+
if (entry.vertex) {
|
|
885
|
+
entry.vertex.position.set(pos.x, pos.y, pos.z);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Update persistent data immediately after transform changes
|
|
890
|
+
this._updateFeaturePersistentData();
|
|
891
|
+
|
|
892
|
+
this._rebuildPreviewLine();
|
|
893
|
+
this._updateExtensionLinesForAllPoints();
|
|
894
|
+
this._notifySplineChange("transform", { selection: targetId });
|
|
895
|
+
this._renderOnce();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
_handleTransformDragging(isDragging) {
|
|
899
|
+
const dragging = !!isDragging;
|
|
900
|
+
|
|
901
|
+
this._isTransformDragging = dragging;
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
if (this.viewer?.controls) {
|
|
905
|
+
this.viewer.controls.enabled = !dragging;
|
|
906
|
+
}
|
|
907
|
+
} catch (error) {
|
|
908
|
+
/* ignore */
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Important: Do NOT clear transforms when dragging stops!
|
|
912
|
+
// This was causing the gizmos to disappear after dragging
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
_updateExtensionLinesForAllPoints() {
|
|
916
|
+
// Update extension lines for all points based on current spline data
|
|
917
|
+
this._splineData.points.forEach((pt) => {
|
|
918
|
+
this._updateExtensionLinesForPoint(pt);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
_updateExtensionLinesForPoint(pt) {
|
|
923
|
+
const forwardKey = `forward-line:${pt.id}`;
|
|
924
|
+
const backwardKey = `backward-line:${pt.id}`;
|
|
925
|
+
|
|
926
|
+
const forwardLine = this._extensionLines.get(forwardKey);
|
|
927
|
+
const backwardLine = this._extensionLines.get(backwardKey);
|
|
928
|
+
|
|
929
|
+
if (!forwardLine || !backwardLine) return;
|
|
930
|
+
|
|
931
|
+
// Get direction from rotation matrix
|
|
932
|
+
const rotation = pt.rotation || [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
933
|
+
let xAxisDirection = [rotation[0], rotation[1], rotation[2]];
|
|
934
|
+
|
|
935
|
+
const length = Math.sqrt(xAxisDirection[0] * xAxisDirection[0] + xAxisDirection[1] * xAxisDirection[1] + xAxisDirection[2] * xAxisDirection[2]);
|
|
936
|
+
if (length > 0) {
|
|
937
|
+
xAxisDirection = [xAxisDirection[0] / length, xAxisDirection[1] / length, xAxisDirection[2] / length];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const forwardDir = pt.flipDirection ? [-xAxisDirection[0], -xAxisDirection[1], -xAxisDirection[2]] : xAxisDirection;
|
|
941
|
+
const backwardDir = pt.flipDirection ? xAxisDirection : [-xAxisDirection[0], -xAxisDirection[1], -xAxisDirection[2]];
|
|
942
|
+
|
|
943
|
+
// Update forward line geometry
|
|
944
|
+
if (pt.forwardDistance > 0) {
|
|
945
|
+
const forwardEnd = [
|
|
946
|
+
pt.position[0] + forwardDir[0] * pt.forwardDistance,
|
|
947
|
+
pt.position[1] + forwardDir[1] * pt.forwardDistance,
|
|
948
|
+
pt.position[2] + forwardDir[2] * pt.forwardDistance
|
|
949
|
+
];
|
|
950
|
+
|
|
951
|
+
const forwardPosAttr = forwardLine.geometry.getAttribute("position");
|
|
952
|
+
if (!forwardPosAttr) {
|
|
953
|
+
forwardLine.geometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
954
|
+
pt.position[0], pt.position[1], pt.position[2],
|
|
955
|
+
forwardEnd[0], forwardEnd[1], forwardEnd[2]
|
|
956
|
+
], 3));
|
|
957
|
+
} else {
|
|
958
|
+
const arr = forwardPosAttr.array;
|
|
959
|
+
arr[0] = pt.position[0]; arr[1] = pt.position[1]; arr[2] = pt.position[2];
|
|
960
|
+
arr[3] = forwardEnd[0]; arr[4] = forwardEnd[1]; arr[5] = forwardEnd[2];
|
|
961
|
+
forwardPosAttr.needsUpdate = true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Update backward line geometry
|
|
966
|
+
if (pt.backwardDistance > 0) {
|
|
967
|
+
const backwardEnd = [
|
|
968
|
+
pt.position[0] + backwardDir[0] * pt.backwardDistance,
|
|
969
|
+
pt.position[1] + backwardDir[1] * pt.backwardDistance,
|
|
970
|
+
pt.position[2] + backwardDir[2] * pt.backwardDistance
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
const backwardPosAttr = backwardLine.geometry.getAttribute("position");
|
|
974
|
+
if (!backwardPosAttr) {
|
|
975
|
+
backwardLine.geometry.setAttribute("position", new THREE.Float32BufferAttribute([
|
|
976
|
+
pt.position[0], pt.position[1], pt.position[2],
|
|
977
|
+
backwardEnd[0], backwardEnd[1], backwardEnd[2]
|
|
978
|
+
], 3));
|
|
979
|
+
} else {
|
|
980
|
+
const arr = backwardPosAttr.array;
|
|
981
|
+
arr[0] = pt.position[0]; arr[1] = pt.position[1]; arr[2] = pt.position[2];
|
|
982
|
+
arr[3] = backwardEnd[0]; arr[4] = backwardEnd[1]; arr[5] = backwardEnd[2];
|
|
983
|
+
backwardPosAttr.needsUpdate = true;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
}
|