brep-io-kernel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -0
- package/README.md +144 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/dist-kernel/help/CONTRIBUTING.html +248 -0
- package/dist-kernel/help/LICENSE.html +248 -0
- package/dist-kernel/help/MODELING.png +0 -0
- package/dist-kernel/help/PMI.png +0 -0
- package/dist-kernel/help/SKETCH.png +0 -0
- package/dist-kernel/help/assembly-constraints__Coincident_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Angle_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Distance_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Fixed_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Parallel_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints___Touch_Align_Constraint_dialog.png +0 -0
- package/dist-kernel/help/assembly-constraints__angle-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__coincident-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__distance-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__fixed-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__parallel-constraint.html +248 -0
- package/dist-kernel/help/assembly-constraints__solver.html +248 -0
- package/dist-kernel/help/assembly-constraints__touch-align-constraint.html +248 -0
- package/dist-kernel/help/brep-api.html +263 -0
- package/dist-kernel/help/brep-kernel.html +258 -0
- package/dist-kernel/help/brep-model.html +248 -0
- package/dist-kernel/help/cylindrical-face-radius-embedding.html +290 -0
- package/dist-kernel/help/dialog-screenshots.html +248 -0
- package/dist-kernel/help/extruded-sketch-radius-embedding.html +336 -0
- package/dist-kernel/help/features__Assembly_Component_dialog.png +0 -0
- package/dist-kernel/help/features__Boolean_dialog.png +0 -0
- package/dist-kernel/help/features__Chamfer_dialog.png +0 -0
- package/dist-kernel/help/features__Datium_dialog.png +0 -0
- package/dist-kernel/help/features__Extrude_dialog.png +0 -0
- package/dist-kernel/help/features__Fillet_dialog.png +0 -0
- package/dist-kernel/help/features__Helix_dialog.png +0 -0
- package/dist-kernel/help/features__Hole_dialog.png +0 -0
- package/dist-kernel/help/features__Image_Heightmap_Solid_dialog.png +0 -0
- package/dist-kernel/help/features__Image_to_Face_dialog.png +0 -0
- package/dist-kernel/help/features__Import_3D_Model_dialog.png +0 -0
- package/dist-kernel/help/features__Loft_dialog.png +0 -0
- package/dist-kernel/help/features__Mirror_dialog.png +0 -0
- package/dist-kernel/help/features__Offset_Shell_dialog.png +0 -0
- package/dist-kernel/help/features__Overlap_Cleanup_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_Linear_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_Radial_dialog.png +0 -0
- package/dist-kernel/help/features__Pattern_dialog.png +0 -0
- package/dist-kernel/help/features__Plane_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cone_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cube_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Cylinder_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Pyramid_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Sphere_dialog.png +0 -0
- package/dist-kernel/help/features__Primitive_Torus_dialog.png +0 -0
- package/dist-kernel/help/features__Remesh_dialog.png +0 -0
- package/dist-kernel/help/features__Revolve_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Contour_Flange_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Cutout_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Flange_dialog.png +0 -0
- package/dist-kernel/help/features__Sheet_Metal_Tab_dialog.png +0 -0
- package/dist-kernel/help/features__Sketch_dialog.png +0 -0
- package/dist-kernel/help/features__Spline_dialog.png +0 -0
- package/dist-kernel/help/features__Sweep_dialog.png +0 -0
- package/dist-kernel/help/features__Transform_dialog.png +0 -0
- package/dist-kernel/help/features__Tube_dialog.png +0 -0
- package/dist-kernel/help/features__assembly-component.html +248 -0
- package/dist-kernel/help/features__boolean.html +248 -0
- package/dist-kernel/help/features__chamfer.html +248 -0
- package/dist-kernel/help/features__datium.html +248 -0
- package/dist-kernel/help/features__datum.html +248 -0
- package/dist-kernel/help/features__extrude.html +248 -0
- package/dist-kernel/help/features__fillet.html +248 -0
- package/dist-kernel/help/features__helix.html +248 -0
- package/dist-kernel/help/features__hole.html +248 -0
- package/dist-kernel/help/features__image-heightmap-solid.html +248 -0
- package/dist-kernel/help/features__image-to-face-2D_dialog.png +0 -0
- package/dist-kernel/help/features__image-to-face-3D_dialog.png +0 -0
- package/dist-kernel/help/features__image-to-face.html +248 -0
- package/dist-kernel/help/features__import-3d-model.html +248 -0
- package/dist-kernel/help/features__index.html +248 -0
- package/dist-kernel/help/features__loft.html +248 -0
- package/dist-kernel/help/features__mirror.html +248 -0
- package/dist-kernel/help/features__offset-shell.html +248 -0
- package/dist-kernel/help/features__pattern-linear.html +248 -0
- package/dist-kernel/help/features__pattern-radial.html +248 -0
- package/dist-kernel/help/features__pattern.html +248 -0
- package/dist-kernel/help/features__plane.html +248 -0
- package/dist-kernel/help/features__primitive-cone.html +248 -0
- package/dist-kernel/help/features__primitive-cube.html +248 -0
- package/dist-kernel/help/features__primitive-cylinder.html +248 -0
- package/dist-kernel/help/features__primitive-pyramid.html +248 -0
- package/dist-kernel/help/features__primitive-sphere.html +248 -0
- package/dist-kernel/help/features__primitive-torus.html +248 -0
- package/dist-kernel/help/features__remesh.html +248 -0
- package/dist-kernel/help/features__revolve.html +248 -0
- package/dist-kernel/help/features__sheet-metal-contour-flange.html +248 -0
- package/dist-kernel/help/features__sheet-metal-flange.html +248 -0
- package/dist-kernel/help/features__sheet-metal-tab.html +248 -0
- package/dist-kernel/help/features__sketch.html +248 -0
- package/dist-kernel/help/features__spline.html +248 -0
- package/dist-kernel/help/features__sweep.html +248 -0
- package/dist-kernel/help/features__transform.html +248 -0
- package/dist-kernel/help/features__tube.html +248 -0
- package/dist-kernel/help/file-formats.html +248 -0
- package/dist-kernel/help/getting-started.html +248 -0
- package/dist-kernel/help/highlights.html +248 -0
- package/dist-kernel/help/history-systems.html +248 -0
- package/dist-kernel/help/how-it-works.html +248 -0
- package/dist-kernel/help/index.html +862 -0
- package/dist-kernel/help/input-params-schema.html +363 -0
- package/dist-kernel/help/inspector-improvements.html +248 -0
- package/dist-kernel/help/inspector.html +248 -0
- package/dist-kernel/help/modes__modeling.html +248 -0
- package/dist-kernel/help/modes__pmi.html +248 -0
- package/dist-kernel/help/modes__sketch.html +248 -0
- package/dist-kernel/help/plugins.html +248 -0
- package/dist-kernel/help/pmi-annotations__Angle_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Explode_Body_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Hole_Callout_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Leader_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Linear_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Note_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__Radial_Dimension_dialog.png +0 -0
- package/dist-kernel/help/pmi-annotations__angle-dimension.html +248 -0
- package/dist-kernel/help/pmi-annotations__explode-body.html +248 -0
- package/dist-kernel/help/pmi-annotations__hole-callout.html +248 -0
- package/dist-kernel/help/pmi-annotations__index.html +248 -0
- package/dist-kernel/help/pmi-annotations__leader.html +248 -0
- package/dist-kernel/help/pmi-annotations__linear-dimension.html +248 -0
- package/dist-kernel/help/pmi-annotations__note.html +248 -0
- package/dist-kernel/help/pmi-annotations__radial-dimension.html +248 -0
- package/dist-kernel/help/search-index.json +464 -0
- package/dist-kernel/help/simplified-radial-dimensions.html +298 -0
- package/dist-kernel/help/solid-methods.html +359 -0
- package/dist-kernel/help/table-of-contents.html +330 -0
- package/dist-kernel/help/ui-overview.html +248 -0
- package/dist-kernel/help/whats-new.html +248 -0
- package/package.json +54 -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,1388 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
const THREE = BREP.THREE;
|
|
3
|
+
import { LineGeometry } from "three/examples/jsm/Addons.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RESOLUTION,
|
|
6
|
+
normalizeSplineData,
|
|
7
|
+
buildHermitePolyline,
|
|
8
|
+
cloneSplineData,
|
|
9
|
+
} from "./splineUtils.js";
|
|
10
|
+
import { SplineEditorSession } from "./SplineEditorSession.js";
|
|
11
|
+
|
|
12
|
+
function renderSplinePointsWidget({ ui, key, controlWrap, row }) {
|
|
13
|
+
const normalizeNumber = (value) => {
|
|
14
|
+
const num = Number(value);
|
|
15
|
+
return Number.isFinite(num) ? num : 0;
|
|
16
|
+
};
|
|
17
|
+
const formatNumber = (value) => {
|
|
18
|
+
const num = Number(value);
|
|
19
|
+
if (!Number.isFinite(num)) return "0";
|
|
20
|
+
return num.toFixed(3).replace(/\.?0+$/, "") || "0";
|
|
21
|
+
};
|
|
22
|
+
const getFeatureID = () => ui?.params?.featureID != null ? String(ui.params.featureID) : null;
|
|
23
|
+
const getViewer = () => ui?.options?.viewer || null;
|
|
24
|
+
const getPartHistory = () =>
|
|
25
|
+
ui?.options?.partHistory || ui?.options?.viewer?.partHistory || null;
|
|
26
|
+
const getFeatureRef = () => {
|
|
27
|
+
const featureID = getFeatureID();
|
|
28
|
+
if (!featureID) return null;
|
|
29
|
+
const viaOption = ui?.options?.featureRef || null;
|
|
30
|
+
if (
|
|
31
|
+
viaOption &&
|
|
32
|
+
String(viaOption?.inputParams?.featureID ?? "") === featureID
|
|
33
|
+
) {
|
|
34
|
+
return viaOption;
|
|
35
|
+
}
|
|
36
|
+
const ph = getPartHistory();
|
|
37
|
+
if (ph && Array.isArray(ph.features)) {
|
|
38
|
+
return (
|
|
39
|
+
ph.features.find(
|
|
40
|
+
(f) => String(f?.inputParams?.featureID ?? "") === featureID
|
|
41
|
+
) || null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
const markDirty = (feature, data) => {
|
|
47
|
+
if (!feature) return;
|
|
48
|
+
feature.lastRunInputParams = {};
|
|
49
|
+
feature.timestamp = 0;
|
|
50
|
+
feature.dirty = true;
|
|
51
|
+
feature.persistentData = feature.persistentData || {};
|
|
52
|
+
feature.persistentData.spline = cloneSplineData(data);
|
|
53
|
+
};
|
|
54
|
+
const computeSignature = (data) => {
|
|
55
|
+
let json;
|
|
56
|
+
try {
|
|
57
|
+
json = JSON.stringify(data);
|
|
58
|
+
} catch {
|
|
59
|
+
return String(Date.now());
|
|
60
|
+
}
|
|
61
|
+
let hash = 0;
|
|
62
|
+
for (let i = 0; i < json.length; i++) {
|
|
63
|
+
hash = (hash * 31 + json.charCodeAt(i)) | 0;
|
|
64
|
+
}
|
|
65
|
+
return `${json.length}:${hash >>> 0}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const host = document.createElement("div");
|
|
69
|
+
host.className = "spline-widget";
|
|
70
|
+
host.dataset.splineWidget = "true";
|
|
71
|
+
const style = document.createElement("style");
|
|
72
|
+
style.textContent = `
|
|
73
|
+
.spline-widget {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 10px;
|
|
77
|
+
width: 100%;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
}
|
|
80
|
+
.spline-widget .spw-header {
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: flex-start;
|
|
83
|
+
gap: 8px;
|
|
84
|
+
flex-wrap: wrap;
|
|
85
|
+
}
|
|
86
|
+
.spline-widget .spw-point-list {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: 8px;
|
|
90
|
+
width: 100%;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
}
|
|
93
|
+
.spline-widget .spw-point-row {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 8px;
|
|
97
|
+
align-items: stretch;
|
|
98
|
+
padding: 10px;
|
|
99
|
+
border-radius: 6px;
|
|
100
|
+
background: rgba(255, 255, 255, 0.04);
|
|
101
|
+
width: 100%;
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
}
|
|
104
|
+
.spline-widget .spw-row-header {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
gap: 8px;
|
|
109
|
+
flex-wrap: wrap;
|
|
110
|
+
width: 100%;
|
|
111
|
+
box-sizing: border-box;
|
|
112
|
+
}
|
|
113
|
+
.spline-widget .spw-selected {
|
|
114
|
+
background: rgba(58, 74, 109, 0.35);
|
|
115
|
+
}
|
|
116
|
+
.spline-widget .spw-title {
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
font-size: 12px;
|
|
119
|
+
color: rgba(255, 255, 255, 0.88);
|
|
120
|
+
text-decoration: underline;
|
|
121
|
+
text-underline-offset: 2px;
|
|
122
|
+
}
|
|
123
|
+
.spline-widget .spw-posline {
|
|
124
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
color: rgba(255, 255, 255, 0.75);
|
|
127
|
+
}
|
|
128
|
+
.spline-widget .spw-coords {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: 1fr;
|
|
131
|
+
gap: 6px;
|
|
132
|
+
width: 100%;
|
|
133
|
+
box-sizing: border-box;
|
|
134
|
+
}
|
|
135
|
+
.spline-widget .spw-axis {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
font-size: 12px;
|
|
141
|
+
color: rgba(255, 255, 255, 0.7);
|
|
142
|
+
width: 100%;
|
|
143
|
+
box-sizing: border-box;
|
|
144
|
+
}
|
|
145
|
+
.spline-widget .spw-axis input {
|
|
146
|
+
width: 100%;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
box-sizing: border-box;
|
|
150
|
+
padding: 4px 6px;
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
153
|
+
background: rgba(0, 0, 0, 0.3);
|
|
154
|
+
color: inherit;
|
|
155
|
+
font-family: inherit;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
}
|
|
158
|
+
.spline-widget .spw-axis input:focus {
|
|
159
|
+
outline: none;
|
|
160
|
+
border-color: rgba(108, 195, 255, 0.9);
|
|
161
|
+
box-shadow: 0 0 0 1px rgba(108, 195, 255, 0.35);
|
|
162
|
+
}
|
|
163
|
+
.spline-widget .spw-actions {
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 6px;
|
|
166
|
+
justify-content: flex-end;
|
|
167
|
+
flex-wrap: wrap;
|
|
168
|
+
}
|
|
169
|
+
.spline-widget .spw-btn,
|
|
170
|
+
.spline-widget .spw-icon-btn,
|
|
171
|
+
.spline-widget .spw-link {
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
padding: 6px 10px;
|
|
177
|
+
background: rgba(108, 195, 255, 0.12);
|
|
178
|
+
color: rgba(223, 239, 255, 0.95);
|
|
179
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
180
|
+
}
|
|
181
|
+
.spline-widget .spw-btn:hover,
|
|
182
|
+
.spline-widget .spw-icon-btn:hover,
|
|
183
|
+
.spline-widget .spw-link:hover {
|
|
184
|
+
background: rgba(108, 195, 255, 0.22);
|
|
185
|
+
}
|
|
186
|
+
.spline-widget .spw-icon-btn {
|
|
187
|
+
padding: 4px 6px;
|
|
188
|
+
min-width: 28px;
|
|
189
|
+
text-align: center;
|
|
190
|
+
}
|
|
191
|
+
.spline-widget .spw-icon-btn.danger {
|
|
192
|
+
background: rgba(255, 107, 107, 0.14);
|
|
193
|
+
color: rgba(255, 214, 214, 0.94);
|
|
194
|
+
}
|
|
195
|
+
.spline-widget .spw-icon-btn.danger:hover {
|
|
196
|
+
background: rgba(255, 107, 107, 0.24);
|
|
197
|
+
}
|
|
198
|
+
.spline-widget .spw-link {
|
|
199
|
+
background: none;
|
|
200
|
+
padding: 0;
|
|
201
|
+
color: rgba(108, 195, 255, 0.9);
|
|
202
|
+
}
|
|
203
|
+
.spline-widget .spw-link:hover {
|
|
204
|
+
color: rgba(154, 214, 255, 0.95);
|
|
205
|
+
}
|
|
206
|
+
.spline-widget .spw-empty {
|
|
207
|
+
opacity: 0.6;
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
padding: 6px 0;
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
host.appendChild(style);
|
|
213
|
+
|
|
214
|
+
if (row && typeof row.querySelector === "function") {
|
|
215
|
+
const labelEl = row.querySelector(".label");
|
|
216
|
+
if (labelEl) {
|
|
217
|
+
labelEl.style.alignSelf = "flex-start";
|
|
218
|
+
labelEl.style.paddingTop = "8px";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const header = document.createElement("div");
|
|
223
|
+
header.className = "spw-header";
|
|
224
|
+
const addBtn = document.createElement("button");
|
|
225
|
+
addBtn.type = "button";
|
|
226
|
+
addBtn.className = "spw-btn";
|
|
227
|
+
addBtn.textContent = "+";
|
|
228
|
+
// tooltip for addBtn
|
|
229
|
+
addBtn.title = "Add a new point to the spline";
|
|
230
|
+
header.appendChild(addBtn);
|
|
231
|
+
host.appendChild(header);
|
|
232
|
+
|
|
233
|
+
const pointList = document.createElement("div");
|
|
234
|
+
pointList.className = "spw-point-list";
|
|
235
|
+
host.appendChild(pointList);
|
|
236
|
+
|
|
237
|
+
controlWrap.appendChild(host);
|
|
238
|
+
|
|
239
|
+
const state = {
|
|
240
|
+
spline: null,
|
|
241
|
+
signature: null,
|
|
242
|
+
pendingFocusId: null,
|
|
243
|
+
pendingFocusNode: null,
|
|
244
|
+
session: null,
|
|
245
|
+
selection: null,
|
|
246
|
+
destroyed: false,
|
|
247
|
+
creatingSession: false,
|
|
248
|
+
refreshing: false,
|
|
249
|
+
inSelectionChange: false, // Guard against recursive selection changes
|
|
250
|
+
inSplineChange: false, // Guard against recursive spline changes
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let pointRowMap = new Map();
|
|
254
|
+
let pointButtonMap = new Map();
|
|
255
|
+
|
|
256
|
+
const loadFromSource = () => {
|
|
257
|
+
const feature = getFeatureRef();
|
|
258
|
+
const raw = feature?.persistentData?.spline || null;
|
|
259
|
+
const normalized = normalizeSplineData(raw);
|
|
260
|
+
return cloneSplineData(normalized);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const ensureState = () => {
|
|
264
|
+
if (!state.spline) {
|
|
265
|
+
state.spline = loadFromSource();
|
|
266
|
+
state.signature = computeSignature(state.spline);
|
|
267
|
+
ui.params[key] = state.signature;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const shouldIgnorePointerEvent = (event) => {
|
|
272
|
+
const path =
|
|
273
|
+
typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
274
|
+
for (const el of path) {
|
|
275
|
+
if (el === host) return true;
|
|
276
|
+
if (el && el.dataset && el.dataset.splineWidget === "true") return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const disposeSession = (force = false) => {
|
|
282
|
+
if (!state.session) return;
|
|
283
|
+
|
|
284
|
+
// Always dispose when explicitly requested or forced
|
|
285
|
+
try {
|
|
286
|
+
state.session.dispose();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
/* ignore */
|
|
289
|
+
}
|
|
290
|
+
state.session = null;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleSessionSelectionChange = (id) => {
|
|
294
|
+
if (state.destroyed || state.inSelectionChange) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Guard against recursive calls
|
|
299
|
+
state.inSelectionChange = true;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// CRITICAL FIX: Don't call session.selectObject from within a session selection change event!
|
|
303
|
+
// This was causing infinite loops: session calls this handler -> we call selectObject -> triggers handler again
|
|
304
|
+
state.selection = id || null;
|
|
305
|
+
|
|
306
|
+
renderAll({ fromSession: true });
|
|
307
|
+
} finally {
|
|
308
|
+
state.inSelectionChange = false;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleSessionSplineChange = (nextData, reason = "transform") => {
|
|
313
|
+
if (state.destroyed || state.inSplineChange) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Guard against recursive calls
|
|
318
|
+
state.inSplineChange = true;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
state.spline = cloneSplineData(normalizeSplineData(nextData));
|
|
322
|
+
|
|
323
|
+
// CRITICAL FIX: Always update persistent data when spline changes
|
|
324
|
+
// This ensures transform changes are preserved when parameters change
|
|
325
|
+
const feature = getFeatureRef();
|
|
326
|
+
if (feature) {
|
|
327
|
+
markDirty(feature, state.spline);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// CRITICAL CHANGE: Only update UI, don't trigger feature rebuild during editing
|
|
331
|
+
// The session preview handles the visual updates, feature rebuild happens on dialog close
|
|
332
|
+
|
|
333
|
+
renderAll({ fromSession: true });
|
|
334
|
+
} finally {
|
|
335
|
+
state.inSplineChange = false;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const ensureSession = () => {
|
|
340
|
+
const viewer = getViewer();
|
|
341
|
+
const featureID = getFeatureID();
|
|
342
|
+
|
|
343
|
+
// Prevent creating multiple sessions or infinite loops
|
|
344
|
+
if (state.session || state.creatingSession || state.destroyed) {
|
|
345
|
+
return state.session;
|
|
346
|
+
}
|
|
347
|
+
if (!viewer || !featureID) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
state.creatingSession = true;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
|
|
355
|
+
// Dispose any existing session first
|
|
356
|
+
disposeSession(true); // Force disposal when creating new session
|
|
357
|
+
|
|
358
|
+
const feature = getFeatureRef();
|
|
359
|
+
if (!feature) {
|
|
360
|
+
|
|
361
|
+
state.creatingSession = false;
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const session = new SplineEditorSession(viewer, featureID, {
|
|
366
|
+
featureRef: feature,
|
|
367
|
+
onSplineChange: handleSessionSplineChange,
|
|
368
|
+
onSelectionChange: handleSessionSelectionChange,
|
|
369
|
+
shouldIgnorePointerEvent,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
state.session = session;
|
|
373
|
+
|
|
374
|
+
const res = Number(feature?.inputParams?.curveResolution);
|
|
375
|
+
const preview = Number.isFinite(res) ? Math.max(4, Math.floor(res)) : undefined;
|
|
376
|
+
// Store desired selection before activation (since activation clears it)
|
|
377
|
+
const desiredSelection = state.selection || (state.spline?.points?.[0] ? `point:${state.spline.points[0].id}` : null);
|
|
378
|
+
|
|
379
|
+
session.activate(state.spline, {
|
|
380
|
+
featureRef: feature,
|
|
381
|
+
previewResolution: preview,
|
|
382
|
+
initialSelection: desiredSelection,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
// After activation, restore or set the desired selection
|
|
387
|
+
let currentSelection = desiredSelection;
|
|
388
|
+
|
|
389
|
+
if (currentSelection) {
|
|
390
|
+
|
|
391
|
+
session.selectObject(currentSelection);
|
|
392
|
+
} else {
|
|
393
|
+
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
state.selection = currentSelection;
|
|
397
|
+
|
|
398
|
+
// Force transform controls to be visible by triggering a rebuild
|
|
399
|
+
// This ensures the controls appear immediately when the session is first created
|
|
400
|
+
if (currentSelection) {
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
session.setSplineData(state.spline, {
|
|
404
|
+
preserveSelection: true,
|
|
405
|
+
silent: true,
|
|
406
|
+
reason: "initial-selection"
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// Double-check selection is active
|
|
412
|
+
if (session.getSelectedId() !== currentSelection) {
|
|
413
|
+
|
|
414
|
+
session.selectObject(currentSelection);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Force transform visibility update as a final fallback
|
|
418
|
+
if (session._updateTransformVisibility) {
|
|
419
|
+
|
|
420
|
+
session._updateTransformVisibility();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Comprehensive debugging of session state and force rebuild if needed
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
// Check if transform controls exist and are in scene
|
|
428
|
+
let hasVisibleControls = false;
|
|
429
|
+
if (session._transformsById) {
|
|
430
|
+
for (const [id, entry] of session._transformsById.entries()) {
|
|
431
|
+
const control = entry?.control;
|
|
432
|
+
const inScene = !!(control && session.viewer?.scene && session.viewer.scene.children.includes(control));
|
|
433
|
+
|
|
434
|
+
if (control?.enabled && control?.visible && inScene) {
|
|
435
|
+
hasVisibleControls = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// If no visible controls but we have a selection, force another rebuild
|
|
441
|
+
if (!hasVisibleControls && currentSelection && session.isActive()) {
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
// Force cleanup before rebuild
|
|
445
|
+
if (session.forceCleanup) {
|
|
446
|
+
session.forceCleanup();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
session.setSplineData(state.spline, {
|
|
450
|
+
preserveSelection: true,
|
|
451
|
+
silent: true,
|
|
452
|
+
reason: "force-controls-visible"
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Force selection again
|
|
456
|
+
session.selectObject(currentSelection);
|
|
457
|
+
|
|
458
|
+
// Force visibility update
|
|
459
|
+
if (session._updateTransformVisibility) {
|
|
460
|
+
session._updateTransformVisibility();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
}
|
|
465
|
+
}, 100);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
} catch (error) {
|
|
469
|
+
/* ignore */
|
|
470
|
+
disposeSession(true); // Force disposal on error
|
|
471
|
+
} finally {
|
|
472
|
+
state.creatingSession = false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
return state.session;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const focusPendingPoint = () => {
|
|
480
|
+
if (!state.pendingFocusNode) return;
|
|
481
|
+
try {
|
|
482
|
+
state.pendingFocusNode.focus();
|
|
483
|
+
state.pendingFocusNode.select?.();
|
|
484
|
+
} catch {
|
|
485
|
+
/* ignore */
|
|
486
|
+
}
|
|
487
|
+
state.pendingFocusNode = null;
|
|
488
|
+
state.pendingFocusId = null;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Centralized function to activate a point (same as clicking edit button)
|
|
492
|
+
const activatePoint = (pointId) => {
|
|
493
|
+
const keyId = `point:${pointId}`;
|
|
494
|
+
|
|
495
|
+
// Ensure session exists before selecting - this will create transform controls
|
|
496
|
+
let activeSession = state.session;
|
|
497
|
+
const viewer = getViewer();
|
|
498
|
+
const featureID = getFeatureID();
|
|
499
|
+
if (!activeSession && viewer && featureID && !state.creatingSession) {
|
|
500
|
+
activeSession = ensureSession();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (activeSession) {
|
|
504
|
+
// Always force redraw to ensure preview and transform controls are rebuilt
|
|
505
|
+
activeSession.selectObject(keyId, { forceRedraw: true });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
state.selection = keyId;
|
|
509
|
+
updateSelectionStyles();
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const renderPointRows = () => {
|
|
513
|
+
pointList.textContent = "";
|
|
514
|
+
pointRowMap = new Map();
|
|
515
|
+
pointButtonMap = new Map();
|
|
516
|
+
state.pendingFocusNode = null;
|
|
517
|
+
const points = Array.isArray(state.spline?.points)
|
|
518
|
+
? state.spline.points
|
|
519
|
+
: [];
|
|
520
|
+
if (!points.length) {
|
|
521
|
+
const empty = document.createElement("div");
|
|
522
|
+
empty.className = "spw-empty";
|
|
523
|
+
empty.textContent = "No points defined.";
|
|
524
|
+
pointList.appendChild(empty);
|
|
525
|
+
pointRowMap.clear();
|
|
526
|
+
pointButtonMap.clear();
|
|
527
|
+
updateSelectionStyles();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
points.forEach((pt, index) => {
|
|
531
|
+
const keyId = `point:${pt.id}`;
|
|
532
|
+
const rowEl = document.createElement("div");
|
|
533
|
+
rowEl.className = "spw-point-row";
|
|
534
|
+
rowEl.dataset.pointId = String(pt.id);
|
|
535
|
+
rowEl.addEventListener("click", (event) => {
|
|
536
|
+
if (event?.defaultPrevented) return;
|
|
537
|
+
const target = event?.target;
|
|
538
|
+
if (target && typeof target.closest === "function") {
|
|
539
|
+
if (target.closest("button")) return;
|
|
540
|
+
}
|
|
541
|
+
activatePoint(pt.id);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Header: title + actions
|
|
545
|
+
const headerEl = document.createElement('div');
|
|
546
|
+
headerEl.className = 'spw-row-header';
|
|
547
|
+
const title = document.createElement("div");
|
|
548
|
+
title.className = "spw-title";
|
|
549
|
+
title.textContent = `Point ${index + 1}`;
|
|
550
|
+
headerEl.appendChild(title);
|
|
551
|
+
|
|
552
|
+
// Actions
|
|
553
|
+
const actions = document.createElement("div");
|
|
554
|
+
actions.className = "spw-actions";
|
|
555
|
+
|
|
556
|
+
const selectBtn = document.createElement("button");
|
|
557
|
+
selectBtn.type = "button";
|
|
558
|
+
selectBtn.className = "spw-btn";
|
|
559
|
+
selectBtn.textContent = "🖉";
|
|
560
|
+
selectBtn.addEventListener("click", () => {
|
|
561
|
+
activatePoint(pt.id);
|
|
562
|
+
});
|
|
563
|
+
actions.appendChild(selectBtn);
|
|
564
|
+
pointButtonMap.set(keyId, selectBtn);
|
|
565
|
+
|
|
566
|
+
const flipBtn = document.createElement("button");
|
|
567
|
+
flipBtn.type = "button";
|
|
568
|
+
flipBtn.className = "spw-btn";
|
|
569
|
+
flipBtn.textContent = ">|<";
|
|
570
|
+
flipBtn.title = "Toggle spline direction";
|
|
571
|
+
flipBtn.addEventListener("click", () => {
|
|
572
|
+
activatePoint(pt.id);
|
|
573
|
+
state.spline.points[index].flipDirection = !state.spline.points[index].flipDirection;
|
|
574
|
+
commit("flip-direction");
|
|
575
|
+
});
|
|
576
|
+
actions.appendChild(flipBtn);
|
|
577
|
+
|
|
578
|
+
const upBtn = document.createElement("button");
|
|
579
|
+
upBtn.type = "button";
|
|
580
|
+
upBtn.className = "spw-icon-btn";
|
|
581
|
+
upBtn.textContent = "△";
|
|
582
|
+
upBtn.title = "Move up";
|
|
583
|
+
if (index === 0) upBtn.disabled = true;
|
|
584
|
+
upBtn.addEventListener("click", () => {
|
|
585
|
+
activatePoint(pt.id);
|
|
586
|
+
movePoint(index, -1);
|
|
587
|
+
});
|
|
588
|
+
actions.appendChild(upBtn);
|
|
589
|
+
|
|
590
|
+
const downBtn = document.createElement("button");
|
|
591
|
+
downBtn.type = "button";
|
|
592
|
+
downBtn.className = "spw-icon-btn";
|
|
593
|
+
downBtn.textContent = "▽";
|
|
594
|
+
downBtn.title = "Move down";
|
|
595
|
+
if (index === points.length - 1) downBtn.disabled = true;
|
|
596
|
+
downBtn.addEventListener("click", () => {
|
|
597
|
+
activatePoint(pt.id);
|
|
598
|
+
movePoint(index, 1);
|
|
599
|
+
});
|
|
600
|
+
actions.appendChild(downBtn);
|
|
601
|
+
|
|
602
|
+
const removeBtn = document.createElement("button");
|
|
603
|
+
removeBtn.type = "button";
|
|
604
|
+
removeBtn.className = "spw-icon-btn danger";
|
|
605
|
+
removeBtn.textContent = "✕";
|
|
606
|
+
removeBtn.title = "Remove point";
|
|
607
|
+
if (points.length <= 2) removeBtn.disabled = true;
|
|
608
|
+
removeBtn.addEventListener("click", () => {
|
|
609
|
+
activatePoint(pt.id);
|
|
610
|
+
removePoint(index);
|
|
611
|
+
});
|
|
612
|
+
actions.appendChild(removeBtn);
|
|
613
|
+
|
|
614
|
+
headerEl.appendChild(actions);
|
|
615
|
+
rowEl.appendChild(headerEl);
|
|
616
|
+
|
|
617
|
+
// Extension Distances section
|
|
618
|
+
const extensionSection = document.createElement("div");
|
|
619
|
+
extensionSection.className = "spw-section";
|
|
620
|
+
const extensionTitle = document.createElement("div");
|
|
621
|
+
extensionTitle.className = "spw-section-title";
|
|
622
|
+
extensionTitle.textContent = "Extension Distances";
|
|
623
|
+
extensionSection.appendChild(extensionTitle);
|
|
624
|
+
|
|
625
|
+
const extensionCoords = document.createElement("div");
|
|
626
|
+
extensionCoords.className = "spw-coords";
|
|
627
|
+
|
|
628
|
+
// Forward distance
|
|
629
|
+
const forwardWrap = document.createElement("label");
|
|
630
|
+
forwardWrap.className = "spw-axis";
|
|
631
|
+
forwardWrap.textContent = "Forward:";
|
|
632
|
+
const forwardInput = document.createElement("input");
|
|
633
|
+
forwardInput.type = "number";
|
|
634
|
+
forwardInput.step = "0.1";
|
|
635
|
+
forwardInput.min = "0";
|
|
636
|
+
forwardInput.value = formatNumber(pt.forwardDistance ?? 1.0);
|
|
637
|
+
forwardInput.addEventListener("change", () => {
|
|
638
|
+
activatePoint(pt.id);
|
|
639
|
+
const next = Math.max(0, normalizeNumber(forwardInput.value));
|
|
640
|
+
if (pt.forwardDistance === next) return;
|
|
641
|
+
state.spline.points[index].forwardDistance = next;
|
|
642
|
+
commit("update-forward-distance");
|
|
643
|
+
});
|
|
644
|
+
forwardInput.addEventListener("focus", () => {
|
|
645
|
+
forwardInput.select?.();
|
|
646
|
+
});
|
|
647
|
+
forwardWrap.appendChild(forwardInput);
|
|
648
|
+
extensionCoords.appendChild(forwardWrap);
|
|
649
|
+
|
|
650
|
+
// Backward distance
|
|
651
|
+
const backwardWrap = document.createElement("label");
|
|
652
|
+
backwardWrap.className = "spw-axis";
|
|
653
|
+
backwardWrap.textContent = "Backward:";
|
|
654
|
+
const backwardInput = document.createElement("input");
|
|
655
|
+
backwardInput.type = "number";
|
|
656
|
+
backwardInput.step = "0.1";
|
|
657
|
+
backwardInput.min = "0";
|
|
658
|
+
backwardInput.value = formatNumber(pt.backwardDistance ?? 1.0);
|
|
659
|
+
backwardInput.addEventListener("change", () => {
|
|
660
|
+
activatePoint(pt.id);
|
|
661
|
+
const next = Math.max(0, normalizeNumber(backwardInput.value));
|
|
662
|
+
if (pt.backwardDistance === next) return;
|
|
663
|
+
state.spline.points[index].backwardDistance = next;
|
|
664
|
+
commit("update-backward-distance");
|
|
665
|
+
});
|
|
666
|
+
backwardInput.addEventListener("focus", () => {
|
|
667
|
+
backwardInput.select?.();
|
|
668
|
+
});
|
|
669
|
+
backwardWrap.appendChild(backwardInput);
|
|
670
|
+
extensionCoords.appendChild(backwardWrap);
|
|
671
|
+
|
|
672
|
+
extensionSection.appendChild(extensionCoords);
|
|
673
|
+
rowEl.appendChild(extensionSection);
|
|
674
|
+
|
|
675
|
+
pointList.appendChild(rowEl);
|
|
676
|
+
pointRowMap.set(keyId, rowEl);
|
|
677
|
+
});
|
|
678
|
+
updateSelectionStyles();
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const updateSelectionStyles = () => {
|
|
682
|
+
const selected = state.selection || null;
|
|
683
|
+
for (const [key, rowEl] of pointRowMap.entries()) {
|
|
684
|
+
rowEl.classList.toggle('spw-selected', selected === key);
|
|
685
|
+
}
|
|
686
|
+
for (const [key, btn] of pointButtonMap.entries()) {
|
|
687
|
+
const isSelected = selected === key;
|
|
688
|
+
btn.style.background = isSelected ? 'rgba(58, 74, 109, 0.45)' : 'rgba(108, 195, 255, 0.12)';
|
|
689
|
+
btn.style.opacity = isSelected ? '1' : '0.95';
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const renderAll = ({ fromSession = false } = {}) => {
|
|
694
|
+
const viewer = getViewer();
|
|
695
|
+
const featureID = getFeatureID();
|
|
696
|
+
|
|
697
|
+
if (state.destroyed || state.creatingSession) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
ensureState();
|
|
702
|
+
|
|
703
|
+
// Always ensure session exists when we have viewer and featureID (but not during updates from session)
|
|
704
|
+
let activeSession = state.session;
|
|
705
|
+
if (!fromSession && viewer && featureID && !state.creatingSession) {
|
|
706
|
+
if (!activeSession) {
|
|
707
|
+
activeSession = ensureSession();
|
|
708
|
+
} else {
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (activeSession && !fromSession) {
|
|
715
|
+
state.selection = activeSession.getSelectedId?.() || state.selection;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
renderPointRows();
|
|
719
|
+
|
|
720
|
+
addBtn.disabled = !getFeatureRef();
|
|
721
|
+
focusPendingPoint();
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const movePoint = (index, delta) => {
|
|
725
|
+
const points = Array.isArray(state.spline?.points)
|
|
726
|
+
? state.spline.points
|
|
727
|
+
: [];
|
|
728
|
+
const nextIndex = index + delta;
|
|
729
|
+
if (nextIndex < 0 || nextIndex >= points.length) return;
|
|
730
|
+
const [item] = points.splice(index, 1);
|
|
731
|
+
points.splice(nextIndex, 0, item);
|
|
732
|
+
state.pendingFocusId = item.id;
|
|
733
|
+
commit("reorder-point", { preserveSelection: true });
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const removePoint = (index) => {
|
|
737
|
+
const points = Array.isArray(state.spline?.points)
|
|
738
|
+
? state.spline.points
|
|
739
|
+
: [];
|
|
740
|
+
if (points.length <= 2) return;
|
|
741
|
+
|
|
742
|
+
const [removed] = points.splice(index, 1);
|
|
743
|
+
|
|
744
|
+
// Determine fallback selection after removal
|
|
745
|
+
let newSelection = null;
|
|
746
|
+
if (points.length > 0) {
|
|
747
|
+
const fallbackIdx = Math.min(index, points.length - 1);
|
|
748
|
+
const fallback = points[fallbackIdx];
|
|
749
|
+
if (fallback) {
|
|
750
|
+
newSelection = `point:${fallback.id}`;
|
|
751
|
+
state.pendingFocusId = fallback.id;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
commit("remove-point", {
|
|
756
|
+
preserveSelection: false,
|
|
757
|
+
newSelection: newSelection
|
|
758
|
+
});
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const commitChangesToFeature = () => {
|
|
762
|
+
|
|
763
|
+
const normalized = normalizeSplineData(state.spline);
|
|
764
|
+
state.spline = cloneSplineData(normalized);
|
|
765
|
+
|
|
766
|
+
const oldSignature = state.signature;
|
|
767
|
+
state.signature = computeSignature(state.spline);
|
|
768
|
+
|
|
769
|
+
ui.params[key] = state.signature;
|
|
770
|
+
|
|
771
|
+
const feature = getFeatureRef();
|
|
772
|
+
markDirty(feature, state.spline);
|
|
773
|
+
|
|
774
|
+
const ph = getPartHistory();
|
|
775
|
+
if (ph && Array.isArray(ph.features)) {
|
|
776
|
+
for (const item of ph.features) {
|
|
777
|
+
if (
|
|
778
|
+
String(item?.inputParams?.featureID ?? "") === featureID &&
|
|
779
|
+
item !== feature
|
|
780
|
+
) {
|
|
781
|
+
markDirty(item, state.spline);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
ui._emitParamsChange(key, {
|
|
787
|
+
signature: state.signature,
|
|
788
|
+
reason: "dialog-close",
|
|
789
|
+
timestamp: Date.now(),
|
|
790
|
+
});
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const commit = (reason, options = {}) => {
|
|
794
|
+
|
|
795
|
+
const { skipSessionSync = false, preserveSelection = true, newSelection = null } = options;
|
|
796
|
+
const focusId = state.pendingFocusId || null;
|
|
797
|
+
const normalized = normalizeSplineData(state.spline);
|
|
798
|
+
state.spline = cloneSplineData(normalized);
|
|
799
|
+
state.pendingFocusId = focusId;
|
|
800
|
+
|
|
801
|
+
// For manual commits (add/remove/reorder points), we do need to update the feature
|
|
802
|
+
// But for transform operations, we rely on preview mode
|
|
803
|
+
|
|
804
|
+
const oldSignature = state.signature;
|
|
805
|
+
state.signature = computeSignature(state.spline);
|
|
806
|
+
|
|
807
|
+
ui.params[key] = state.signature;
|
|
808
|
+
|
|
809
|
+
const feature = getFeatureRef();
|
|
810
|
+
markDirty(feature, state.spline);
|
|
811
|
+
|
|
812
|
+
const ph = getPartHistory();
|
|
813
|
+
const featureID = getFeatureID();
|
|
814
|
+
if (ph && Array.isArray(ph.features)) {
|
|
815
|
+
for (const item of ph.features) {
|
|
816
|
+
if (
|
|
817
|
+
String(item?.inputParams?.featureID ?? "") === featureID &&
|
|
818
|
+
item !== feature
|
|
819
|
+
) {
|
|
820
|
+
markDirty(item, state.spline);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (!skipSessionSync && !state.creatingSession) {
|
|
826
|
+
// For structural changes (add/remove/reorder points), restart the session completely
|
|
827
|
+
const isStructuralChange = reason === "add-point" || reason === "remove-point" || reason === "reorder-point";
|
|
828
|
+
|
|
829
|
+
if (isStructuralChange) {
|
|
830
|
+
console.log(`SplineWidget: Restarting session due to structural change: ${reason}`);
|
|
831
|
+
|
|
832
|
+
// Completely dispose and recreate the session
|
|
833
|
+
disposeSession(true);
|
|
834
|
+
state.session = null;
|
|
835
|
+
|
|
836
|
+
// Create new session with updated spline data
|
|
837
|
+
const session = ensureSession();
|
|
838
|
+
if (session && newSelection) {
|
|
839
|
+
session.selectObject(newSelection);
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
// For non-structural changes, just update the existing session
|
|
843
|
+
const session = ensureSession();
|
|
844
|
+
if (session) {
|
|
845
|
+
session.setFeatureRef(feature);
|
|
846
|
+
session.setSplineData(state.spline, {
|
|
847
|
+
preserveSelection,
|
|
848
|
+
silent: true,
|
|
849
|
+
reason,
|
|
850
|
+
});
|
|
851
|
+
if (newSelection) session.selectObject(newSelection);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (skipSessionSync && newSelection) {
|
|
856
|
+
state.selection = newSelection;
|
|
857
|
+
}
|
|
858
|
+
if (!state.session && newSelection) {
|
|
859
|
+
state.selection = newSelection;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// CRITICAL: Never trigger feature rebuild while session is active!
|
|
863
|
+
// The session handles all preview updates. Feature rebuild only happens when dialog closes.
|
|
864
|
+
// This prevents Spline.run() from being called while editing, which would interfere with the session.
|
|
865
|
+
|
|
866
|
+
// Only save the signature change, but don't emit params change to prevent feature rebuild
|
|
867
|
+
// Feature will be rebuilt when the dialog closes via commitChangesToFeature()
|
|
868
|
+
renderAll();
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
addBtn.addEventListener("click", () => {
|
|
872
|
+
ensureState();
|
|
873
|
+
const points = Array.isArray(state.spline?.points)
|
|
874
|
+
? state.spline.points
|
|
875
|
+
: [];
|
|
876
|
+
|
|
877
|
+
if (points.length === 0) {
|
|
878
|
+
// If no points exist, add first point at origin
|
|
879
|
+
const newPoint = {
|
|
880
|
+
id: `p${Date.now().toString(36)}${Math.random()
|
|
881
|
+
.toString(36)
|
|
882
|
+
.slice(2, 6)}`,
|
|
883
|
+
position: [0, 0, 0],
|
|
884
|
+
forwardDistance: 1.0,
|
|
885
|
+
backwardDistance: 1.0,
|
|
886
|
+
};
|
|
887
|
+
points.push(newPoint);
|
|
888
|
+
state.pendingFocusId = newPoint.id;
|
|
889
|
+
commit("add-point", {
|
|
890
|
+
preserveSelection: false,
|
|
891
|
+
newSelection: `point:${newPoint.id}`
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Helper function to find midpoint along polyline and get segment direction
|
|
897
|
+
const findPolylineMidpoint = (p0, p1, t0, t1) => {
|
|
898
|
+
// Generate the polyline between these two points using current spline settings
|
|
899
|
+
const tempSpline = {
|
|
900
|
+
points: [p0, p1]
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const bendRadius = Number.isFinite(Number(state.spline?.bendRadius))
|
|
904
|
+
? Math.max(0.1, Math.min(5.0, Number(state.spline.bendRadius)))
|
|
905
|
+
: 1.0;
|
|
906
|
+
|
|
907
|
+
// Use buildHermitePolyline to get the actual line segments
|
|
908
|
+
const { positions } = buildHermitePolyline(tempSpline, 20, bendRadius); // Use reasonable resolution
|
|
909
|
+
|
|
910
|
+
if (positions.length < 6) {
|
|
911
|
+
// Not enough points, fall back to simple midpoint
|
|
912
|
+
return {
|
|
913
|
+
position: [
|
|
914
|
+
(p0.position[0] + p1.position[0]) / 2,
|
|
915
|
+
(p0.position[1] + p1.position[1]) / 2,
|
|
916
|
+
(p0.position[2] + p1.position[2]) / 2
|
|
917
|
+
],
|
|
918
|
+
direction: [1, 0, 0] // Default direction
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Calculate total polyline length
|
|
923
|
+
let totalLength = 0;
|
|
924
|
+
const segmentLengths = [];
|
|
925
|
+
for (let i = 0; i < positions.length - 3; i += 3) {
|
|
926
|
+
const dx = positions[i + 3] - positions[i];
|
|
927
|
+
const dy = positions[i + 4] - positions[i + 1];
|
|
928
|
+
const dz = positions[i + 5] - positions[i + 2];
|
|
929
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
930
|
+
segmentLengths.push(length);
|
|
931
|
+
totalLength += length;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Find segment at 50% distance
|
|
935
|
+
const targetDistance = totalLength * 0.5;
|
|
936
|
+
let accumulatedDistance = 0;
|
|
937
|
+
|
|
938
|
+
for (let i = 0; i < segmentLengths.length; i++) {
|
|
939
|
+
if (accumulatedDistance + segmentLengths[i] >= targetDistance) {
|
|
940
|
+
// This segment contains the midpoint
|
|
941
|
+
const segmentStart = i * 3;
|
|
942
|
+
const segmentEnd = segmentStart + 3;
|
|
943
|
+
|
|
944
|
+
// Calculate position along this segment
|
|
945
|
+
const remainingDistance = targetDistance - accumulatedDistance;
|
|
946
|
+
const t = remainingDistance / segmentLengths[i];
|
|
947
|
+
|
|
948
|
+
const startPos = [positions[segmentStart], positions[segmentStart + 1], positions[segmentStart + 2]];
|
|
949
|
+
const endPos = [positions[segmentEnd], positions[segmentEnd + 1], positions[segmentEnd + 2]];
|
|
950
|
+
|
|
951
|
+
const position = [
|
|
952
|
+
startPos[0] + t * (endPos[0] - startPos[0]),
|
|
953
|
+
startPos[1] + t * (endPos[1] - startPos[1]),
|
|
954
|
+
startPos[2] + t * (endPos[2] - startPos[2])
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
// Calculate segment direction
|
|
958
|
+
const direction = [
|
|
959
|
+
endPos[0] - startPos[0],
|
|
960
|
+
endPos[1] - startPos[1],
|
|
961
|
+
endPos[2] - startPos[2]
|
|
962
|
+
];
|
|
963
|
+
|
|
964
|
+
// Normalize direction
|
|
965
|
+
const length = Math.sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]);
|
|
966
|
+
if (length > 0) {
|
|
967
|
+
direction[0] /= length;
|
|
968
|
+
direction[1] /= length;
|
|
969
|
+
direction[2] /= length;
|
|
970
|
+
} else {
|
|
971
|
+
direction[0] = 1; direction[1] = 0; direction[2] = 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return { position, direction };
|
|
975
|
+
}
|
|
976
|
+
accumulatedDistance += segmentLengths[i];
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Fallback to end of polyline
|
|
980
|
+
const lastIndex = positions.length - 3;
|
|
981
|
+
return {
|
|
982
|
+
position: [positions[lastIndex], positions[lastIndex + 1], positions[lastIndex + 2]],
|
|
983
|
+
direction: [1, 0, 0]
|
|
984
|
+
};
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// Helper function to create rotation matrix from direction vector
|
|
988
|
+
const createRotationFromDirection = (direction) => {
|
|
989
|
+
// Use direction as X-axis (forward direction)
|
|
990
|
+
const xAxis = [...direction];
|
|
991
|
+
|
|
992
|
+
// Create Y-axis perpendicular to direction
|
|
993
|
+
// Try Y-up first, but use Z-up if direction is already along Y
|
|
994
|
+
let yAxis;
|
|
995
|
+
if (Math.abs(direction[1]) < 0.9) {
|
|
996
|
+
// Direction is not along Y, use Y-up
|
|
997
|
+
yAxis = [0, 1, 0];
|
|
998
|
+
} else {
|
|
999
|
+
// Direction is along Y, use Z-up
|
|
1000
|
+
yAxis = [0, 0, 1];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Cross product to get perpendicular axis
|
|
1004
|
+
const cross = (a, b) => [
|
|
1005
|
+
a[1] * b[2] - a[2] * b[1],
|
|
1006
|
+
a[2] * b[0] - a[0] * b[2],
|
|
1007
|
+
a[0] * b[1] - a[1] * b[0]
|
|
1008
|
+
];
|
|
1009
|
+
|
|
1010
|
+
// Calculate Z-axis = X cross Y
|
|
1011
|
+
let zAxis = cross(xAxis, yAxis);
|
|
1012
|
+
let zLength = Math.sqrt(zAxis[0] * zAxis[0] + zAxis[1] * zAxis[1] + zAxis[2] * zAxis[2]);
|
|
1013
|
+
if (zLength > 0) {
|
|
1014
|
+
zAxis = [zAxis[0] / zLength, zAxis[1] / zLength, zAxis[2] / zLength];
|
|
1015
|
+
} else {
|
|
1016
|
+
zAxis = [0, 0, 1];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Recalculate Y-axis = Z cross X to ensure orthogonality
|
|
1020
|
+
yAxis = cross(zAxis, xAxis);
|
|
1021
|
+
let yLength = Math.sqrt(yAxis[0] * yAxis[0] + yAxis[1] * yAxis[1] + yAxis[2] * yAxis[2]);
|
|
1022
|
+
if (yLength > 0) {
|
|
1023
|
+
yAxis = [yAxis[0] / yLength, yAxis[1] / yLength, yAxis[2] / yLength];
|
|
1024
|
+
} else {
|
|
1025
|
+
yAxis = [0, 1, 0];
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Return rotation matrix as flat array
|
|
1029
|
+
return [
|
|
1030
|
+
xAxis[0], xAxis[1], xAxis[2],
|
|
1031
|
+
yAxis[0], yAxis[1], yAxis[2],
|
|
1032
|
+
zAxis[0], zAxis[1], zAxis[2]
|
|
1033
|
+
];
|
|
1034
|
+
}; // Helper function to calculate tangent vector from point data
|
|
1035
|
+
const calculateTangent = (pointData, isForward) => {
|
|
1036
|
+
const rotation = pointData.rotation || [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
1037
|
+
let direction = [rotation[0], rotation[1], rotation[2]]; // X-axis from rotation matrix
|
|
1038
|
+
|
|
1039
|
+
if (pointData.flipDirection) {
|
|
1040
|
+
direction = [-direction[0], -direction[1], -direction[2]];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const distance = isForward ? pointData.forwardDistance : pointData.backwardDistance;
|
|
1044
|
+
const tangentDir = isForward ? direction : [-direction[0], -direction[1], -direction[2]];
|
|
1045
|
+
|
|
1046
|
+
return [
|
|
1047
|
+
tangentDir[0] * distance,
|
|
1048
|
+
tangentDir[1] * distance,
|
|
1049
|
+
tangentDir[2] * distance
|
|
1050
|
+
];
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// Find the currently selected point
|
|
1054
|
+
let selectedIndex = -1;
|
|
1055
|
+
const selectedId = state.selection?.replace("point:", "") || null;
|
|
1056
|
+
if (selectedId) {
|
|
1057
|
+
selectedIndex = points.findIndex(p => p.id === selectedId);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Determine insertion position and calculate new point position
|
|
1061
|
+
let insertIndex;
|
|
1062
|
+
let newPosition;
|
|
1063
|
+
let newRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Default identity matrix
|
|
1064
|
+
|
|
1065
|
+
if (selectedIndex === -1 || selectedIndex === points.length - 1) {
|
|
1066
|
+
// No selection or last point selected - insert before last point
|
|
1067
|
+
if (points.length === 1) {
|
|
1068
|
+
// Only one point exists, add second point offset from first
|
|
1069
|
+
const base = points[0].position;
|
|
1070
|
+
newPosition = [base[0] + 2, base[1], base[2]];
|
|
1071
|
+
insertIndex = points.length; // Add at end
|
|
1072
|
+
// Keep default orientation for second point
|
|
1073
|
+
} else {
|
|
1074
|
+
insertIndex = points.length - 1; // Insert before last
|
|
1075
|
+
// Find point and direction along polyline between second-to-last and last point
|
|
1076
|
+
const p0 = points[points.length - 2];
|
|
1077
|
+
const p1 = points[points.length - 1];
|
|
1078
|
+
const t0 = calculateTangent(p0, true); // Forward tangent of first point
|
|
1079
|
+
const t1 = calculateTangent(p1, false); // Backward tangent of second point
|
|
1080
|
+
const result = findPolylineMidpoint(p0, p1, t0, t1);
|
|
1081
|
+
newPosition = result.position;
|
|
1082
|
+
newRotation = createRotationFromDirection(result.direction);
|
|
1083
|
+
}
|
|
1084
|
+
} else {
|
|
1085
|
+
// Insert after selected point
|
|
1086
|
+
insertIndex = selectedIndex + 1;
|
|
1087
|
+
if (insertIndex >= points.length) {
|
|
1088
|
+
// Selected point is last, add at end offset from selected
|
|
1089
|
+
const base = points[selectedIndex].position;
|
|
1090
|
+
newPosition = [base[0] + 2, base[1], base[2]];
|
|
1091
|
+
// Keep default orientation when adding at end
|
|
1092
|
+
} else {
|
|
1093
|
+
// Find point and direction along polyline between selected point and next point
|
|
1094
|
+
const p0 = points[selectedIndex];
|
|
1095
|
+
const p1 = points[selectedIndex + 1];
|
|
1096
|
+
const t0 = calculateTangent(p0, true); // Forward tangent of first point
|
|
1097
|
+
const t1 = calculateTangent(p1, false); // Backward tangent of second point
|
|
1098
|
+
const result = findPolylineMidpoint(p0, p1, t0, t1);
|
|
1099
|
+
newPosition = result.position;
|
|
1100
|
+
newRotation = createRotationFromDirection(result.direction);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const newPoint = {
|
|
1105
|
+
id: `p${Date.now().toString(36)}${Math.random()
|
|
1106
|
+
.toString(36)
|
|
1107
|
+
.slice(2, 6)}`,
|
|
1108
|
+
position: newPosition,
|
|
1109
|
+
rotation: newRotation,
|
|
1110
|
+
forwardDistance: 0.1,
|
|
1111
|
+
backwardDistance: 0.1,
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// Insert the point at the calculated position
|
|
1115
|
+
points.splice(insertIndex, 0, newPoint);
|
|
1116
|
+
state.pendingFocusId = newPoint.id;
|
|
1117
|
+
commit("add-point", {
|
|
1118
|
+
preserveSelection: false,
|
|
1119
|
+
newSelection: `point:${newPoint.id}`
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
ensureState();
|
|
1124
|
+
|
|
1125
|
+
// Set up initial selection to first point if no selection exists
|
|
1126
|
+
if (!state.selection && state.spline?.points?.length > 0) {
|
|
1127
|
+
const firstPoint = state.spline.points[0];
|
|
1128
|
+
state.selection = `point:${firstPoint.id}`;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
renderAll();
|
|
1132
|
+
|
|
1133
|
+
// If session creation failed initially due to missing viewer/featureID, retry multiple times
|
|
1134
|
+
if (!state.session) {
|
|
1135
|
+
|
|
1136
|
+
// Try multiple times with increasing delays
|
|
1137
|
+
const retryDelays = [50, 200, 500, 1000]; // Try at 50ms, 200ms, 500ms, and 1s
|
|
1138
|
+
|
|
1139
|
+
retryDelays.forEach((delay, index) => {
|
|
1140
|
+
setTimeout(() => {
|
|
1141
|
+
if (!state.session && !state.destroyed) {
|
|
1142
|
+
renderAll();
|
|
1143
|
+
|
|
1144
|
+
// If we successfully created a session and have a selection, make sure transform controls are visible
|
|
1145
|
+
if (state.session && state.selection) {
|
|
1146
|
+
state.session.selectObject(state.selection);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}, delay);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return {
|
|
1154
|
+
inputEl: host,
|
|
1155
|
+
inputRegistered: false,
|
|
1156
|
+
skipDefaultRefresh: true,
|
|
1157
|
+
refreshFromParams() {
|
|
1158
|
+
if (state.destroyed || state.creatingSession || state.refreshing) return;
|
|
1159
|
+
|
|
1160
|
+
const viewer = getViewer();
|
|
1161
|
+
const featureID = getFeatureID();
|
|
1162
|
+
|
|
1163
|
+
const stack = new Error().stack;
|
|
1164
|
+
state.refreshing = true;
|
|
1165
|
+
|
|
1166
|
+
try {
|
|
1167
|
+
const next = loadFromSource();
|
|
1168
|
+
const nextSig = computeSignature(next);
|
|
1169
|
+
if (nextSig !== state.signature) {
|
|
1170
|
+
state.spline = next;
|
|
1171
|
+
state.signature = nextSig;
|
|
1172
|
+
ui.params[key] = state.signature;
|
|
1173
|
+
|
|
1174
|
+
// Only update existing session, don't create new one during refresh
|
|
1175
|
+
if (state.session) {
|
|
1176
|
+
state.session.setFeatureRef(getFeatureRef());
|
|
1177
|
+
state.session.setSplineData(state.spline, {
|
|
1178
|
+
preserveSelection: true,
|
|
1179
|
+
silent: true,
|
|
1180
|
+
});
|
|
1181
|
+
state.selection = state.session.getSelectedId?.() || state.selection;
|
|
1182
|
+
}
|
|
1183
|
+
renderAll({ fromSession: true });
|
|
1184
|
+
} else if (state.session) {
|
|
1185
|
+
// Only update existing session
|
|
1186
|
+
state.session.setFeatureRef(getFeatureRef());
|
|
1187
|
+
renderAll({ fromSession: true });
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Try to create session if it doesn't exist and viewer/featureID are now available
|
|
1191
|
+
if (!state.session && viewer && featureID && !state.creatingSession) {
|
|
1192
|
+
renderAll(); // This will create the session
|
|
1193
|
+
}
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
/* ignore */
|
|
1196
|
+
} finally {
|
|
1197
|
+
// Use setTimeout to prevent rapid successive calls - increase delay to break loops
|
|
1198
|
+
setTimeout(() => {
|
|
1199
|
+
state.refreshing = false;
|
|
1200
|
+
}, 200); // Increased from 50ms to 200ms to break refresh loops
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
destroy() {
|
|
1204
|
+
// CRITICAL: Commit all changes to the feature when dialog closes
|
|
1205
|
+
if (!state.destroyed && state.spline) {
|
|
1206
|
+
commitChangesToFeature();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
state.destroyed = true;
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
disposeSession(true); // Force disposal during destroy
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
},
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const inputParamsSchema = {
|
|
1223
|
+
id: {
|
|
1224
|
+
type: "string",
|
|
1225
|
+
default_value: null,
|
|
1226
|
+
hint: "unique identifier for the spline feature",
|
|
1227
|
+
},
|
|
1228
|
+
curveResolution: {
|
|
1229
|
+
type: "number",
|
|
1230
|
+
default_value: DEFAULT_RESOLUTION,
|
|
1231
|
+
hint: "Samples per segment used to visualize the spline",
|
|
1232
|
+
},
|
|
1233
|
+
bendRadius: {
|
|
1234
|
+
type: "number",
|
|
1235
|
+
default_value: 1.0,
|
|
1236
|
+
label: "Bend Radius",
|
|
1237
|
+
hint: "Controls the smoothness of curve transitions. Lower values create sharper bends, higher values create smoother curves.",
|
|
1238
|
+
min: 0.1,
|
|
1239
|
+
step: 0.5,
|
|
1240
|
+
},
|
|
1241
|
+
splinePoints: {
|
|
1242
|
+
type: "string",
|
|
1243
|
+
label: "Spline Points",
|
|
1244
|
+
hint: "Add, reorder, and position spline anchors",
|
|
1245
|
+
renderWidget: renderSplinePointsWidget,
|
|
1246
|
+
},
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
export class SplineFeature {
|
|
1250
|
+
static shortName = "SP";
|
|
1251
|
+
static longName = "Spline";
|
|
1252
|
+
static inputParamsSchema = inputParamsSchema;
|
|
1253
|
+
|
|
1254
|
+
constructor() {
|
|
1255
|
+
this.inputParams = {};
|
|
1256
|
+
this.persistentData = this.persistentData || {};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
destroy() {
|
|
1260
|
+
// Dispose the spline session if it exists to remove preview and transform controls
|
|
1261
|
+
if (this._splineSession) {
|
|
1262
|
+
this._splineSession.dispose();
|
|
1263
|
+
this._splineSession = null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Mark as destroyed to prevent further operations
|
|
1267
|
+
this._destroyed = true;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
_ensureSplineData() {
|
|
1273
|
+
const source = this.persistentData?.spline || null;
|
|
1274
|
+
const normalized = normalizeSplineData(source);
|
|
1275
|
+
this.persistentData = this.persistentData || {};
|
|
1276
|
+
this.persistentData.spline = normalized;
|
|
1277
|
+
return normalized;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async run(partHistory) {
|
|
1281
|
+
const spline = this._ensureSplineData();
|
|
1282
|
+
const featureId = this.inputParams?.featureID
|
|
1283
|
+
? String(this.inputParams.featureID)
|
|
1284
|
+
: "Spline";
|
|
1285
|
+
|
|
1286
|
+
const sceneGroup = new THREE.Group();
|
|
1287
|
+
sceneGroup.name = featureId;
|
|
1288
|
+
sceneGroup.type = "SKETCH";
|
|
1289
|
+
sceneGroup.onClick = () => { };
|
|
1290
|
+
|
|
1291
|
+
const resolution = Number.isFinite(Number(this.inputParams?.curveResolution))
|
|
1292
|
+
? Math.max(4, Number(this.inputParams.curveResolution))
|
|
1293
|
+
: DEFAULT_RESOLUTION;
|
|
1294
|
+
|
|
1295
|
+
const bendRadius = Number.isFinite(Number(this.inputParams?.bendRadius))
|
|
1296
|
+
? Math.max(0.1, Math.min(5.0, Number(this.inputParams.bendRadius)))
|
|
1297
|
+
: 1.0;
|
|
1298
|
+
|
|
1299
|
+
const { positions, polyline } = buildHermitePolyline(spline, resolution, bendRadius);
|
|
1300
|
+
|
|
1301
|
+
if (positions.length >= 6) {
|
|
1302
|
+
const geometry = new LineGeometry();
|
|
1303
|
+
geometry.setPositions(positions);
|
|
1304
|
+
|
|
1305
|
+
const edge = new BREP.Edge(geometry);
|
|
1306
|
+
edge.name = `${featureId}:SplineEdge`;
|
|
1307
|
+
edge.userData = {
|
|
1308
|
+
polylineLocal: polyline.map((p) => [p[0], p[1], p[2]]),
|
|
1309
|
+
polylineWorld: true,
|
|
1310
|
+
splineFeatureId: featureId,
|
|
1311
|
+
};
|
|
1312
|
+
sceneGroup.add(edge);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
try {
|
|
1316
|
+
const vertices = spline.points.map((pt, idx) => {
|
|
1317
|
+
const vertex = new BREP.Vertex(pt.position, {
|
|
1318
|
+
name: `${featureId}:P${idx}`,
|
|
1319
|
+
});
|
|
1320
|
+
vertex.userData = vertex.userData || {};
|
|
1321
|
+
vertex.userData.splineFeatureId = featureId;
|
|
1322
|
+
vertex.userData.splinePointId = pt.id;
|
|
1323
|
+
return vertex;
|
|
1324
|
+
});
|
|
1325
|
+
for (const v of vertices) {
|
|
1326
|
+
sceneGroup.add(v);
|
|
1327
|
+
}
|
|
1328
|
+
} catch {
|
|
1329
|
+
// optional vertices failed; ignore
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
// Add extension handles as vertices for visualization
|
|
1334
|
+
spline.points.forEach((pt, idx) => {
|
|
1335
|
+
const forwardPos = [
|
|
1336
|
+
pt.position[0] + pt.forwardExtension[0],
|
|
1337
|
+
pt.position[1] + pt.forwardExtension[1],
|
|
1338
|
+
pt.position[2] + pt.forwardExtension[2]
|
|
1339
|
+
];
|
|
1340
|
+
const backwardPos = [
|
|
1341
|
+
pt.position[0] + pt.backwardExtension[0],
|
|
1342
|
+
pt.position[1] + pt.backwardExtension[1],
|
|
1343
|
+
pt.position[2] + pt.backwardExtension[2]
|
|
1344
|
+
];
|
|
1345
|
+
|
|
1346
|
+
const forwardVertex = new BREP.Vertex(forwardPos, {
|
|
1347
|
+
name: `${featureId}:F${idx}`,
|
|
1348
|
+
});
|
|
1349
|
+
forwardVertex.userData = {
|
|
1350
|
+
splineFeatureId: featureId,
|
|
1351
|
+
splinePointId: pt.id,
|
|
1352
|
+
extensionType: "forward",
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
const backwardVertex = new BREP.Vertex(backwardPos, {
|
|
1356
|
+
name: `${featureId}:B${idx}`,
|
|
1357
|
+
});
|
|
1358
|
+
backwardVertex.userData = {
|
|
1359
|
+
splineFeatureId: featureId,
|
|
1360
|
+
splinePointId: pt.id,
|
|
1361
|
+
extensionType: "backward",
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
sceneGroup.add(forwardVertex);
|
|
1365
|
+
sceneGroup.add(backwardVertex);
|
|
1366
|
+
});
|
|
1367
|
+
} catch {
|
|
1368
|
+
/* ignore extension vertex creation failure */
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
this.persistentData = this.persistentData || {};
|
|
1372
|
+
this.persistentData.spline = cloneSplineData(spline);
|
|
1373
|
+
|
|
1374
|
+
// remove all children of the scene that have a name starting with "SplineEditorPreview"
|
|
1375
|
+
const existingPreviews = partHistory.scene.children.filter(child =>
|
|
1376
|
+
|
|
1377
|
+
child.name.startsWith("SplineEditor")
|
|
1378
|
+
);
|
|
1379
|
+
for (const preview of existingPreviews) {
|
|
1380
|
+
preview.userData.preventRemove = false;
|
|
1381
|
+
partHistory.scene.remove(preview);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
return { added: [sceneGroup], removed: [] };
|
|
1387
|
+
}
|
|
1388
|
+
}
|