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