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,1550 @@
1
+ // PMIMode.js
2
+ // Lightweight PMI editing mode modeled after SketchMode3D UI patterns.
3
+ // - Hides Viewer sidebar and main toolbar
4
+ // - Adds a top-right Finish control
5
+ // - Adds a simple top toolbar for annotation tools
6
+ // - Adds a right-side overlay panel listing annotations for the current PMI view
7
+ // - Persists annotations back into the PMI view entry on Finish
8
+
9
+ import * as THREE from 'three';
10
+ import { annotationRegistry } from './AnnotationRegistry.js';
11
+ import { getPMIStyle, setPMIStyle, sanitizePMIStyle } from './pmiStyle.js';
12
+ import { AnnotationHistory } from './AnnotationHistory.js';
13
+ import { LabelOverlay } from './LabelOverlay.js';
14
+ import { AnnotationCollectionWidget } from './AnnotationCollectionWidget.js';
15
+ import { localStorage as LS } from '../../idbStorage.js';
16
+
17
+ const cssEscape = (value) => {
18
+ if (window.CSS && typeof window.CSS.escape === 'function') {
19
+ return window.CSS.escape(value);
20
+ }
21
+ return String(value).replace(/"/g, '\\"');
22
+ };
23
+
24
+ // Register built-in annotation types
25
+ export class PMIMode {
26
+ /**
27
+ * @param {Viewer} viewer
28
+ * @param {Object} viewEntry - reference to the PMI view object from PMIViewsWidget
29
+ * @param {number} viewIndex - index of the view in PMIViewsWidget.views
30
+ * @param {PMIViewsWidget} pmiWidget - widget instance for persistence/refresh
31
+ */
32
+ constructor(viewer, viewEntry, viewIndex, pmiWidget) {
33
+ this.viewer = viewer;
34
+ this.viewEntry = (viewEntry && typeof viewEntry === 'object')
35
+ ? viewEntry
36
+ : { viewName: 'View', name: 'View', camera: {}, annotations: [] };
37
+ if (!Array.isArray(this.viewEntry.annotations)) {
38
+ this.viewEntry.annotations = [];
39
+ }
40
+ const resolvedName = typeof this.viewEntry.viewName === 'string'
41
+ ? this.viewEntry.viewName
42
+ : (typeof this.viewEntry.name === 'string' ? this.viewEntry.name : 'View');
43
+ this.viewEntry.viewName = String(resolvedName || 'View').trim() || 'View';
44
+ this.viewEntry.name = this.viewEntry.viewName;
45
+ if (!this.viewEntry.camera || typeof this.viewEntry.camera !== 'object') {
46
+ this.viewEntry.camera = {};
47
+ }
48
+ this.viewIndex = viewIndex;
49
+ this.pmiWidget = pmiWidget;
50
+
51
+ this._uiTopRight = null;
52
+ this._annGroup = null;
53
+ this._originalSections = null;
54
+ this._pmiModeViewsSection = null;
55
+ this._pmiViewsDomRestore = null;
56
+ this._pmiAnnotationsSection = null;
57
+ this._pmiToolOptionsSection = null;
58
+ this._pmiStyleStorageKey = '__PMI_STYLE_SETTINGS__';
59
+ this._sectionCreationPromises = [];
60
+ this._opts = { noteText: '', leaderText: 'TEXT HERE', dimDecimals: 3 };
61
+ this._onCanvasDown = this._handlePointerDown.bind(this);
62
+ this._onControlsChange = this._refreshOverlays.bind(this);
63
+ this._labelOverlay = null; // manages overlay labels
64
+ this._baseMatrixSessionKey = `pmi-base-${Date.now()}-${Math.random().toString(36).slice(2)}`;
65
+ this._hasBaseMatrices = false;
66
+ this._annotationWidget = null;
67
+ this._dragPlaneHelper = null;
68
+
69
+ // Annotation history stores inputParams/persistentData similar to PartHistory
70
+ this._annotationHistory = new AnnotationHistory(this);
71
+ const src = Array.isArray(this.viewEntry.annotations) ? this.viewEntry.annotations : [];
72
+ this._annotationHistory.load(JSON.parse(JSON.stringify(src)));
73
+ this.#loadPMIStyle();
74
+ try {
75
+ for (const entity of this._annotationHistory.getEntries()) {
76
+ try { this.#normalizeAnnotation(entity.inputParams); } catch { }
77
+ if (!entity.runtimeAttributes || typeof entity.runtimeAttributes !== 'object') {
78
+ entity.runtimeAttributes = {};
79
+ }
80
+ entity.runtimeAttributes.__open = false;
81
+ if (entity.inputParams && typeof entity.inputParams === 'object') {
82
+ entity.inputParams.__open = false;
83
+ }
84
+ }
85
+ } catch { }
86
+ }
87
+
88
+ open() {
89
+ const v = this.viewer;
90
+ if (!v || !v.container) return;
91
+
92
+ // Save and hide existing accordion sections instead of hiding the whole sidebar
93
+ this.#hideOriginalSidebarSections();
94
+
95
+ // Build styles once
96
+ this.#ensureStyles();
97
+
98
+ // Mount overlay UI
99
+ this.#mountTopRightControls();
100
+ // Add PMI sections to existing accordion instead of creating a new sidebar
101
+ this.#mountPMISections();
102
+
103
+ // Apply stored view settings for this PMI view (e.g., wireframe)
104
+ try {
105
+ const vs = this.viewEntry?.viewSettings || this.viewEntry?.settings;
106
+ if (vs && typeof vs.wireframe === 'boolean') {
107
+ this.#toggleWireframeMode(Boolean(vs.wireframe));
108
+ }
109
+ } catch { }
110
+
111
+ // Build annotation group and render existing annotations
112
+ this._annGroup = new THREE.Group();
113
+ this._annGroup.name = `__PMI_ANN__:${this.#getViewDisplayName('view')}`;
114
+ this._annGroup.renderOrder = 9995;
115
+ try { v.scene.add(this._annGroup); } catch { }
116
+ this._annotationsDirty = true; // Flag to track when rebuild is needed
117
+ this._lastCameraState = null; // Track camera changes for overlay updates
118
+ this.#rebuildAnnotationObjects();
119
+
120
+ // Remember modeling-space transforms so we can restore/apply PMI offsets deterministically
121
+ this.#ensureBaseSolidMatrices();
122
+ this.#resetSolidsToBaseMatrices();
123
+
124
+ // Apply view-specific transforms from ViewTransform annotations AFTER annotations are processed
125
+ this.#applyViewTransforms();
126
+ // Initialize label overlay manager
127
+ try {
128
+ this._labelOverlay = new LabelOverlay(this.viewer,
129
+ (idx, ann, ev) => this.#startLabelDrag(idx, ann, ev),
130
+ (idx, ann, ev) => this.#focusAnnotationDialog(idx, ann, ev),
131
+ (idx, ann, ev) => this.#handleLabelClick(idx, ann, ev),
132
+ (idx, ann, ev) => this.#handleLabelDragEnd(idx, ann, ev));
133
+ } catch { }
134
+
135
+ // Initial refresh of overlay positions
136
+ setTimeout(() => this._refreshOverlays(), 100);
137
+
138
+ // Periodically refresh to follow model changes, but only if needed
139
+ try {
140
+ this._refreshTimer = setInterval(() => {
141
+ try {
142
+ if (this._annotationsDirty) {
143
+ this.#rebuildAnnotationObjects();
144
+ this._annotationsDirty = false;
145
+ }
146
+ // Also check if camera has changed as fallback for overlay updates
147
+ this.#checkCameraChange();
148
+ } catch { }
149
+ }, 1000);
150
+ } catch { }
151
+
152
+ // Listen on canvas for tool inputs
153
+ // Use capture to preempt Viewer handlers and ArcballControls
154
+ try { v.renderer.domElement.addEventListener('pointerdown', this._onCanvasDown, { passive: false, capture: true }); } catch { }
155
+ // Listen for camera/controls changes to update label positions
156
+ try {
157
+ if (v.controls && typeof this._onControlsChange === 'function') {
158
+ v.controls.addEventListener('change', this._onControlsChange);
159
+ // Some controls use 'end' instead of 'change' for the final position
160
+ if (typeof v.controls.addEventListener === 'function') {
161
+ try { v.controls.addEventListener('end', this._onControlsChange); } catch { }
162
+ }
163
+ }
164
+ } catch { }
165
+
166
+ // Apply camera controls policy based on current tool
167
+ try { this._controlsEnabledPrev = !!v.controls?.enabled; } catch { this._controlsEnabledPrev = true; }
168
+ try { if (v.controls) v.controls.enabled = true; } catch { }
169
+ }
170
+
171
+ applyViewTransformsSequential() {
172
+ try {
173
+ this.#applyViewTransforms();
174
+ this._refreshOverlays();
175
+ } catch (error) {
176
+ console.warn('Failed to apply view transforms sequentially:', error);
177
+ }
178
+ }
179
+
180
+ async finish() {
181
+ // Persist annotations back into the view entry and refresh PMI widget
182
+ try { this.#_persistView(true); } catch { }
183
+ // Immediately return scene solids to modeling state before we notify the viewer
184
+ try { this.#restoreViewTransforms(); } catch { }
185
+ try { this.#resetSolidsToBaseMatrices(); } catch { }
186
+ try { this.viewer.onPMIFinished?.(this.viewEntry); } catch { }
187
+ await this.dispose();
188
+ }
189
+
190
+ async dispose() {
191
+ const v = this.viewer;
192
+
193
+ // Restore original transforms when exiting PMI mode
194
+ this.#restoreViewTransforms();
195
+ this.#resetSolidsToBaseMatrices();
196
+
197
+ try { v.renderer.domElement.removeEventListener('pointerdown', this._onCanvasDown, { capture: true }); } catch { }
198
+ // Remove controls change listeners
199
+ try {
200
+ if (v.controls && typeof this._onControlsChange === 'function') {
201
+ v.controls.removeEventListener('change', this._onControlsChange);
202
+ try { v.controls.removeEventListener('end', this._onControlsChange); } catch { }
203
+ }
204
+ } catch { }
205
+ // Remove overlay UI
206
+ try { this._uiTopRight?.remove(); } catch { }
207
+
208
+ // IMPORTANT: Remove PMI-specific accordion sections FIRST, then restore original sections
209
+ // This prevents visual glitches where both sets of sections are visible simultaneously
210
+ await this.#removePMISections();
211
+
212
+ // Now restore original sidebar sections after PMI sections are completely removed
213
+ this.#restoreOriginalSidebarSections();
214
+
215
+ // Remove annotation group
216
+ try { if (this._annGroup && this._annGroup.parent) this._annGroup.parent.remove(this._annGroup); } catch { }
217
+ this._annGroup = null;
218
+ try { if (this._refreshTimer) clearInterval(this._refreshTimer); } catch { } this._refreshTimer = null;
219
+ try { this.hideDragPlaneHelper(); } catch { }
220
+ // Remove labels overlay and destroy feature UIs
221
+ try { this._labelOverlay?.dispose?.(); } catch { }
222
+ this._labelOverlay = null;
223
+ try { this._annotationWidget?.dispose?.(); } catch { }
224
+
225
+ // Clear PMI base matrices once we're back in modeling mode
226
+ this.#clearBaseSolidMatrices();
227
+
228
+ // Note: Main toolbar is no longer hidden so no restoration needed
229
+ // Restore camera controls enabled state
230
+ try { if (this.viewer?.controls) this.viewer.controls.enabled = !!this._controlsEnabledPrev; } catch { }
231
+ }
232
+
233
+ // Persist the current in-memory annotations back onto the view entry and save via PMI widget
234
+ #_persistView(refreshList = false) {
235
+ try {
236
+ if (!this.viewEntry) return;
237
+ // Serialize annotations using annotation history (inputParams + persistentData)
238
+ const history = this._annotationHistory;
239
+ const baseSerialized = history ? history.toSerializable() : [];
240
+ const entities = history ? history.getEntries() : [];
241
+ const serializedAnnotations = baseSerialized.map((entry, idx) => {
242
+ const entity = entities[idx] || null;
243
+ const ann = entity?.inputParams || null;
244
+ const handler = annotationRegistry.getSafe?.(ann?.type || entry.type) || annotationRegistry.getSafe?.(entry.type) || null;
245
+ if (handler && typeof handler.serialize === 'function') {
246
+ try {
247
+ const custom = handler.serialize(ann, entry, { entity });
248
+ if (custom) return custom;
249
+ } catch {
250
+ // fall back to base entry if serialize throws
251
+ }
252
+ }
253
+ return entry;
254
+ });
255
+ this.viewEntry.annotations = JSON.parse(JSON.stringify(serializedAnnotations));
256
+
257
+ this.#notifyViewMutated(refreshList);
258
+ } catch { /* ignore */ }
259
+ }
260
+
261
+ #notifyViewMutated(refreshList = false) {
262
+ let updated = null;
263
+ try {
264
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
265
+ if (manager) {
266
+ if (Number.isFinite(this.viewIndex) && typeof manager.updateView === 'function') {
267
+ updated = manager.updateView(this.viewIndex, this.viewEntry);
268
+ }
269
+ if (!updated && typeof manager.notifyChanged === 'function') {
270
+ manager.notifyChanged();
271
+ }
272
+ }
273
+ } catch { }
274
+
275
+ if (!updated && this.pmiWidget && Number.isFinite(this.viewIndex) && Array.isArray(this.pmiWidget.views)) {
276
+ this.pmiWidget.views[this.viewIndex] = this.viewEntry;
277
+ }
278
+
279
+ if (refreshList) {
280
+ try { this.pmiWidget?.refreshFromHistory?.(); } catch { }
281
+ try { this.pmiWidget?._renderList?.(); } catch { }
282
+ }
283
+ }
284
+
285
+ #getViewDisplayName(fallback = 'View') {
286
+ const entry = this.viewEntry;
287
+ if (!entry || typeof entry !== 'object') return fallback;
288
+ const nm = typeof entry.viewName === 'string' ? entry.viewName : entry.name;
289
+ const trimmed = String(nm || '').trim();
290
+ return trimmed || fallback;
291
+ }
292
+
293
+ // --- UI construction ---
294
+ #ensureStyles() {
295
+ if (document.getElementById('pmi-mode-styles')) return;
296
+ const style = document.createElement('style');
297
+ style.id = 'pmi-mode-styles';
298
+ style.textContent = `
299
+ /* Top-right buttons */
300
+ .pmi-top-right { position: absolute; top: 48px; right: 0px; display: flex; gap: 8px; z-index: 1001; }
301
+ .pmi-btn { appearance: none; border: 1px solid #262b36; border-radius: 8px; padding: 6px 10px; cursor: pointer; background: rgba(255,255,255,.05); color: #e6e6e6; font-weight: 700; }
302
+ .pmi-btn.primary { background: linear-gradient(180deg, rgba(110,168,254,.25), rgba(110,168,254,.15)); }
303
+
304
+ /* Annotations list */
305
+ .pmi-ann-list { flex: 1 1 auto; overflow: auto; display: flex; flex-direction: column; gap: 4px; }
306
+
307
+ /* Mini accordion for per-annotation dialogs */
308
+ .pmi-acc { display: flex; flex-direction: column; gap: 4px; }
309
+ .pmi-acc-item { background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.01)); border: 1px solid #1f2937; border-radius: 10px; overflow: hidden; }
310
+ .pmi-acc-header { display: grid; grid-template-columns: 1fr auto; align-items: stretch; }
311
+ .pmi-acc-headbtn { appearance: none; width: 100%; text-align: left; background: transparent; color: #e5e7eb; border: 0; padding: 8px 10px; display: flex; align-items: center; gap: 6px; cursor: pointer; }
312
+ .pmi-acc-title { flex: 1; }
313
+ .pmi-acc-status { margin-left: 8px; color: #9ca3af; font-size: 12px; line-height: 1; }
314
+ .pmi-acc-actions { display: flex; align-items: center; gap: 4px; padding: 6px 8px 6px 0; }
315
+ .pmi-acc-content { padding: 8px 10px 10px 10px; border-top: 1px solid #1f2937; }
316
+ .pmi-acc-item.collapsed .pmi-acc-content { display: none; }
317
+ .pmi-acc-del { appearance: none; border: 1px solid #374151; background: rgba(255,255,255,.03); color: #e5e7eb; border-radius: 8px; padding: 4px 8px; cursor: pointer; }
318
+ .pmi-acc-del:hover { border-color: #ef4444; color: #fff; background: rgba(239,68,68,.15); }
319
+
320
+ /* Overlay labels are defined in LabelOverlay.css */
321
+
322
+ /* Form fields for View Settings / Tool Options */
323
+ .pmi-vfield { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
324
+ .pmi-vlabel { color: #9ca3af; font-size: 12px; }
325
+ .pmi-input { border: 1px solid #374151; border-radius: 6px; padding: 4px 6px; }
326
+ .pmi-number { width: 80px; border: 1px solid #374151; border-radius: 6px; padding: 4px 6px; }
327
+ .pmi-vfield .pmi-input, .pmi-vfield .pmi-number { width: 100%; box-sizing: border-box; background: #0b0e14; color: #e5e7eb; border: 1px solid #374151; border-radius: 6px; padding: 6px 8px; }
328
+ .pmi-vcheck { display: flex; align-items: center; gap: 8px; }
329
+ `;
330
+ document.head.appendChild(style);
331
+ }
332
+
333
+ #mountTopRightControls() {
334
+ const host = this.viewer.container;
335
+ host.style.position = host.style.position || 'relative';
336
+ const wrap = document.createElement('div');
337
+ wrap.className = 'pmi-top-right';
338
+
339
+ const btnFinish = document.createElement('button');
340
+ btnFinish.className = 'pmi-btn primary';
341
+ btnFinish.textContent = 'Finish';
342
+ btnFinish.addEventListener('click', () => this.finish());
343
+ wrap.appendChild(btnFinish);
344
+ host.appendChild(wrap);
345
+ this._uiTopRight = wrap;
346
+ }
347
+ #hideOriginalSidebarSections() {
348
+ try {
349
+ const v = this.viewer;
350
+ if (!v || !v.accordion) return;
351
+
352
+ // Store original accordion sections for restoration later
353
+ this._originalSections = [];
354
+ const accordion = v.accordion.uiElement;
355
+
356
+ // Find all accordion sections and hide them
357
+ const titles = accordion.querySelectorAll('.accordion-title');
358
+ const contents = accordion.querySelectorAll('.accordion-content');
359
+
360
+ titles.forEach(title => {
361
+ this._originalSections.push({
362
+ element: title,
363
+ display: title.style.display || '',
364
+ visibility: title.style.visibility || ''
365
+ });
366
+ title.style.display = 'none';
367
+ });
368
+
369
+ contents.forEach(content => {
370
+ this._originalSections.push({
371
+ element: content,
372
+ display: content.style.display || '',
373
+ visibility: content.style.visibility || ''
374
+ });
375
+ content.style.display = 'none';
376
+ });
377
+ } catch (e) {
378
+ console.warn('Failed to hide original sidebar sections:', e);
379
+ }
380
+ }
381
+
382
+ #restoreOriginalSidebarSections() {
383
+ try {
384
+ if (!this._originalSections) {
385
+ return;
386
+ }
387
+
388
+
389
+ // Restore all original sections
390
+ this._originalSections.forEach(({ element, display, visibility }, index) => {
391
+ if (element && element.parentNode) {
392
+ element.style.display = display;
393
+ element.style.visibility = visibility;
394
+ } else {
395
+ console.warn(`Section ${index} element no longer exists in DOM`);
396
+ }
397
+ });
398
+
399
+ this._originalSections = null;
400
+ } catch (e) {
401
+ console.warn('Failed to restore original sidebar sections:', e);
402
+ }
403
+ }
404
+
405
+ async #removePMISections() {
406
+ try {
407
+ const v = this.viewer;
408
+ if (!v || !v.accordion) return;
409
+
410
+ // Wait for any pending section creation to complete first
411
+ if (this._sectionCreationPromises && this._sectionCreationPromises.length > 0) {
412
+ try {
413
+ await Promise.allSettled(this._sectionCreationPromises);
414
+ } catch (e) {
415
+ console.warn('Some section creation promises failed:', e);
416
+ }
417
+ }
418
+
419
+
420
+ // Remove PMI sections from the accordion
421
+ const sectionsToRemove = [
422
+ 'PMI Views (PMI Mode)',
423
+ 'Annotations - ' + this.#getViewDisplayName(''),
424
+ 'View Settings',
425
+ 'PMI Settings'
426
+ ];
427
+
428
+
429
+ if (this._pmiViewsDomRestore && this.pmiWidget?.uiElement) {
430
+ try {
431
+ const widgetEl = this.pmiWidget.uiElement;
432
+ const { parent, next } = this._pmiViewsDomRestore;
433
+ if (widgetEl && parent) {
434
+ if (next && next.parentNode === parent) {
435
+ parent.insertBefore(widgetEl, next);
436
+ } else {
437
+ parent.appendChild(widgetEl);
438
+ }
439
+ }
440
+ } catch (err) {
441
+ console.warn('Failed to restore PMI Views widget before removal:', err);
442
+ }
443
+ }
444
+ this._pmiViewsDomRestore = null;
445
+
446
+ // First, try to use the stored section references for direct removal
447
+ const storedSections = [this._pmiModeViewsSection, this._pmiAnnotationsSection, this._pmiToolOptionsSection];
448
+ storedSections.forEach((section, index) => {
449
+ if (section && section.uiElement) {
450
+ try {
451
+ // Remove the title element
452
+ const titleEl = section.uiElement.previousElementSibling;
453
+ if (titleEl && titleEl.classList.contains('accordion-title')) {
454
+ titleEl.remove();
455
+ }
456
+ // Remove the content element
457
+ section.uiElement.remove();
458
+ } catch (e) {
459
+ console.warn(`Failed to remove stored section ${index}:`, e);
460
+ }
461
+ }
462
+ });
463
+
464
+ // Aggressively search and remove any PMI-related elements
465
+ try {
466
+ const accordion = v.accordion.uiElement;
467
+
468
+ // Look for elements with PMI-related text content
469
+ const allTitles = Array.from(accordion.querySelectorAll('.accordion-title'));
470
+ const allContents = Array.from(accordion.querySelectorAll('.accordion-content'));
471
+
472
+
473
+ // Remove elements that match PMI section patterns
474
+ allTitles.forEach(titleEl => {
475
+ const text = titleEl.textContent || '';
476
+ if (text.includes('Annotations') || text === 'View Settings' || text === 'PMI Settings') {
477
+ // Find and remove the associated content element as well
478
+ const nextEl = titleEl.nextElementSibling;
479
+ if (nextEl && nextEl.classList.contains('accordion-content')) {
480
+ nextEl.remove();
481
+ }
482
+ titleEl.remove();
483
+ }
484
+ });
485
+
486
+ // Remove any remaining content elements that might have been missed
487
+ allContents.forEach(contentEl => {
488
+ if (!contentEl.parentNode) return; // Already removed
489
+ const id = contentEl.id || '';
490
+ const name = contentEl.getAttribute('name') || '';
491
+ if (name.includes('Annotations') || name === 'accordion-content-View Settings' || name === 'accordion-content-PMI Settings' ||
492
+ id.includes('Annotations') || id === 'accordion-content-View Settings' || id === 'accordion-content-PMI Settings') {
493
+ contentEl.remove();
494
+ }
495
+ });
496
+
497
+ // Additional cleanup: remove any elements that contain PMI-specific classes or content
498
+ const pmiElements = accordion.querySelectorAll('.pmi-ann-list, .pmi-scrollable-content, .pmi-inline-menu, .pmi-ann-footer, .pmi-vfield');
499
+ pmiElements.forEach(el => {
500
+ // Remove the entire parent accordion section if this is PMI content
501
+ let parent = el.parentNode;
502
+ while (parent && !parent.classList.contains('accordion-content')) {
503
+ parent = parent.parentNode;
504
+ }
505
+ if (parent && parent.classList.contains('accordion-content')) {
506
+ const titleEl = parent.previousElementSibling;
507
+ if (titleEl && titleEl.classList.contains('accordion-title')) {
508
+ titleEl.remove();
509
+ }
510
+ parent.remove();
511
+ } else {
512
+ el.remove();
513
+ }
514
+ });
515
+
516
+ // Final nuclear option: remove any sections that weren't there originally
517
+ // This is a bit aggressive but ensures complete cleanup
518
+ } catch (e) {
519
+ console.warn('Failed to manually clean up PMI section elements:', e);
520
+ }
521
+
522
+ // Try to remove sections using the accordion API as a fallback
523
+ for (const title of sectionsToRemove) {
524
+ try {
525
+ if (v.accordion && typeof v.accordion.removeSection === 'function') {
526
+ await v.accordion.removeSection(title);
527
+ }
528
+ } catch (e) {
529
+ console.warn(`Failed to remove section "${title}" via API:`, e);
530
+ }
531
+ }
532
+
533
+ // Clear stored section references
534
+ this._pmiModeViewsSection = null;
535
+ this._pmiAnnotationsSection = null;
536
+ this._pmiToolOptionsSection = null;
537
+ this._sectionCreationPromises = [];
538
+
539
+
540
+ } catch (e) {
541
+ console.warn('Failed to remove PMI sections:', e);
542
+ }
543
+ }
544
+
545
+ #mountPMISections() {
546
+ try {
547
+ const v = this.viewer;
548
+ if (!v || !v.accordion) return;
549
+
550
+ // Use the existing accordion instead of creating a new one
551
+ this._acc = v.accordion;
552
+
553
+ const pmiViewsPromise = this._acc.addSection('PMI Views (PMI Mode)').then((sec) => {
554
+ try {
555
+ this._pmiModeViewsSection = sec;
556
+ const titleEl = sec.uiElement.previousElementSibling;
557
+ if (titleEl) {
558
+ titleEl.textContent = 'PMI Views';
559
+ }
560
+
561
+ const widget = this.pmiWidget;
562
+ const widgetEl = widget?.uiElement;
563
+ if (widgetEl) {
564
+ if (!this._pmiViewsDomRestore) {
565
+ this._pmiViewsDomRestore = {
566
+ parent: widgetEl.parentNode || null,
567
+ next: widgetEl.nextSibling || null,
568
+ };
569
+ }
570
+ sec.uiElement.appendChild(widgetEl);
571
+ }
572
+
573
+ this.#applyPMIPanelLayout();
574
+ } catch (e) {
575
+ console.warn('Failed to setup PMI Views section:', e);
576
+ }
577
+ });
578
+ this._sectionCreationPromises.push(pmiViewsPromise);
579
+
580
+ const annotationsPromise = this._acc.addSection(`Annotations - ${this.#getViewDisplayName('')}`).then((sec) => {
581
+ try {
582
+ const widgetWrap = document.createElement('div');
583
+ widgetWrap.className = 'pmi-ann-widget-wrap';
584
+ sec.uiElement.appendChild(widgetWrap);
585
+
586
+ this._annotationWidget = new AnnotationCollectionWidget({
587
+ history: this._annotationHistory,
588
+ pmimode: this,
589
+ onCollectionChange: () => {
590
+ this.#updateAnnotationSectionTitle();
591
+ this.#markAnnotationsDirty();
592
+ },
593
+ onEntryChange: () => {
594
+ this.#updateAnnotationSectionTitle();
595
+ this.#markAnnotationsDirty();
596
+ },
597
+ });
598
+ widgetWrap.appendChild(this._annotationWidget.uiElement);
599
+
600
+ this._pmiAnnotationsSection = sec;
601
+ this.#updateAnnotationSectionTitle();
602
+ this.#applyPMIPanelLayout();
603
+ } catch (e) {
604
+ console.warn('Failed to setup annotations section:', e);
605
+ }
606
+ });
607
+ this._sectionCreationPromises.push(annotationsPromise);
608
+
609
+ // PMI Settings section
610
+ this._toolOptsEl = document.createElement('div');
611
+ this._toolOptsEl.style.padding = '6px';
612
+ const toolOptionsPromise = this._acc.addSection('PMI Settings').then((sec) => {
613
+ try {
614
+ sec.uiElement.appendChild(this._toolOptsEl);
615
+ this.#renderToolOptions();
616
+ this._pmiToolOptionsSection = sec;
617
+ this.#applyPMIPanelLayout();
618
+ } catch (e) {
619
+ console.warn('Failed to setup tool options section:', e);
620
+ }
621
+ });
622
+ this._sectionCreationPromises.push(toolOptionsPromise);
623
+
624
+ this._annotationWidget?.render();
625
+ } catch (e) {
626
+ console.warn('Failed to mount PMI sections:', e);
627
+ }
628
+ }
629
+
630
+ #applyPMIPanelLayout() {
631
+ try {
632
+ const accordion = this.viewer?.accordion?.uiElement;
633
+ if (!accordion) return;
634
+ const sections = [
635
+ this._pmiModeViewsSection,
636
+ this._pmiAnnotationsSection,
637
+ this._pmiToolOptionsSection,
638
+ ];
639
+ const fragment = document.createDocumentFragment();
640
+ let hasAny = false;
641
+ for (const section of sections) {
642
+ if (!section || !section.uiElement) continue;
643
+ const titleEl = section.uiElement.previousElementSibling;
644
+ if (!titleEl) continue;
645
+ fragment.appendChild(titleEl);
646
+ fragment.appendChild(section.uiElement);
647
+ hasAny = true;
648
+ }
649
+ if (!hasAny) return;
650
+ accordion.insertBefore(fragment, accordion.firstChild || null);
651
+ } catch (e) {
652
+ console.warn('Failed to apply PMI panel layout:', e);
653
+ }
654
+ }
655
+
656
+ markAnnotationsDirty() {
657
+ this.#markAnnotationsDirty();
658
+ }
659
+
660
+ normalizeAnnotation(annotation) {
661
+ if (!annotation) return annotation;
662
+ return this.#normalizeAnnotation(annotation);
663
+ }
664
+
665
+ handleAnnotationRemoval(entry) {
666
+ if (!entry) return;
667
+ try {
668
+ const handler = annotationRegistry.getSafe?.(entry.type) || entry.constructor || null;
669
+ const ann = entry.inputParams || {};
670
+ if (handler && typeof handler._resolveSolidReferences === 'function') {
671
+ try { handler._resolveSolidReferences(ann, this, false); } catch { /* ignore */ }
672
+ }
673
+ if (handler && typeof handler.restoreOriginalTransforms === 'function') {
674
+ try { handler.restoreOriginalTransforms(ann, this); } catch { /* ignore */ }
675
+ }
676
+ } catch (error) {
677
+ console.warn('PMI: handleAnnotationRemoval failed:', error);
678
+ }
679
+ try { this.applyViewTransformsSequential?.(); } catch { /* ignore */ }
680
+ }
681
+
682
+ #updateAnnotationSectionTitle() {
683
+ try {
684
+ const sec = this._pmiAnnotationsSection;
685
+ if (!sec || !sec.uiElement) return;
686
+ const titleEl = sec.uiElement.previousElementSibling;
687
+ if (titleEl) {
688
+ titleEl.textContent = `Annotations - ${this.#getViewDisplayName('')}`;
689
+ }
690
+ } catch { /* ignore */ }
691
+ }
692
+
693
+ #renderToolOptions() {
694
+ const el = this._toolOptsEl;
695
+ if (!el) return;
696
+ el.textContent = '';
697
+
698
+ const makeVField = (label, input) => {
699
+ const wrap = document.createElement('div');
700
+ wrap.className = 'pmi-vfield';
701
+ const lab = document.createElement('div');
702
+ lab.className = 'pmi-vlabel';
703
+ lab.textContent = label;
704
+ wrap.appendChild(lab);
705
+ wrap.appendChild(input);
706
+ return wrap;
707
+ };
708
+
709
+ const mkText = (placeholder, value, onChange) => {
710
+ const inp = document.createElement('input');
711
+ inp.type = 'text';
712
+ inp.placeholder = placeholder;
713
+ inp.value = value || '';
714
+ inp.className = 'pmi-input';
715
+ inp.addEventListener('change', () => onChange(inp.value));
716
+ return inp;
717
+ };
718
+
719
+ const mkNumber = (value, onChange, { min = 0, max = 8 } = {}) => {
720
+ const inp = document.createElement('input');
721
+ inp.type = 'number';
722
+ inp.min = String(min); inp.max = String(max);
723
+ inp.value = String(value);
724
+ inp.className = 'pmi-number';
725
+ inp.addEventListener('change', () => {
726
+ let v = Number(inp.value);
727
+ if (!Number.isFinite(v)) v = 3;
728
+ v = Math.max(min, Math.min(max, v));
729
+ onChange(v);
730
+ });
731
+ return inp;
732
+ };
733
+
734
+ const noteDefault = mkText('Default note text', this._opts.noteText, (v) => { this._opts.noteText = v; });
735
+ el.appendChild(makeVField('Note text', noteDefault));
736
+
737
+ const leaderDefault = mkText('Default leader text', this._opts.leaderText, (v) => { this._opts.leaderText = v; });
738
+ el.appendChild(makeVField('Leader text', leaderDefault));
739
+
740
+ const dimDec = mkNumber(this._opts.dimDecimals, (v) => { this._opts.dimDecimals = v | 0; this._annotationWidget?.render(); }, { min: 0, max: 8 });
741
+ el.appendChild(makeVField('Dim decimals', dimDec));
742
+
743
+ // Global PMI style controls
744
+ const style = getPMIStyle();
745
+ const mkColor = (value, onChange) => {
746
+ const wrap = document.createElement('div');
747
+ wrap.className = 'pmi-color-wrap';
748
+ const inp = document.createElement('input');
749
+ inp.type = 'color';
750
+ inp.value = toHex(value);
751
+ inp.className = 'pmi-color';
752
+ inp.addEventListener('input', () => onChange(inp.value));
753
+ wrap.appendChild(inp);
754
+ return wrap;
755
+ };
756
+
757
+ const updateStyle = (patch) => {
758
+ setPMIStyle(patch);
759
+ this.#markAnnotationsDirty();
760
+ this._annotationWidget?.render();
761
+ this.#savePMIStyle();
762
+ };
763
+
764
+ el.appendChild(makeVField('Line color', mkColor(style.lineColor, (v) => updateStyle({ lineColor: hexToInt(v) }))));
765
+ el.appendChild(makeVField('Dot color', mkColor(style.dotColor, (v) => updateStyle({ dotColor: hexToInt(v) }))));
766
+ el.appendChild(makeVField('Arrow color', mkColor(style.arrowColor, (v) => updateStyle({ arrowColor: hexToInt(v) }))));
767
+ const mkPxNumber = (label, key, min = 1, max = 64) => {
768
+ const inp = mkNumber(style[key] ?? 0, (v) => updateStyle({ [key]: v }), { min, max });
769
+ el.appendChild(makeVField(label, inp));
770
+ };
771
+ mkPxNumber('Line width', 'lineWidth', 1, 8);
772
+ mkPxNumber('Arrow length (px)', 'arrowLengthPx', 1, 64);
773
+ mkPxNumber('Arrow width (px)', 'arrowWidthPx', 1, 32);
774
+ mkPxNumber('Leader dot (px)', 'leaderDotRadiusPx', 1, 32);
775
+ mkPxNumber('Hole dot (px)', 'holeDotRadiusPx', 1, 32);
776
+ mkPxNumber('Note dot radius', 'noteDotRadius', 0.01, 1);
777
+ }
778
+
779
+ #loadPMIStyle() {
780
+ try {
781
+ const raw = LS.getItem(this._pmiStyleStorageKey);
782
+ if (!raw) return;
783
+ const parsed = JSON.parse(raw);
784
+ if (parsed && typeof parsed === 'object') {
785
+ setPMIStyle(sanitizePMIStyle(parsed));
786
+ }
787
+ } catch (e) {
788
+ console.warn('[PMI] Failed to load PMI style settings:', e);
789
+ }
790
+ }
791
+
792
+ #savePMIStyle() {
793
+ try {
794
+ const style = sanitizePMIStyle(getPMIStyle());
795
+ LS.setItem(this._pmiStyleStorageKey, JSON.stringify(style));
796
+ } catch (e) {
797
+ console.warn('[PMI] Failed to save PMI style settings:', e);
798
+ }
799
+ }
800
+
801
+ #toggleWireframeMode(isWireframe) {
802
+ try {
803
+ const scene = this.viewer?.scene;
804
+ if (!scene) return;
805
+
806
+ const isFace = (obj) => !!obj && (obj.type === 'FACE' || typeof obj.userData?.faceName === 'string');
807
+ const apply = (mat) => { if (mat && 'wireframe' in mat) mat.wireframe = !!isWireframe; };
808
+ // Toggle wireframe only on face materials.
809
+ scene.traverse((obj) => {
810
+ if (!isFace(obj)) return;
811
+ const m = obj.material;
812
+ if (!m) return;
813
+ if (Array.isArray(m)) m.forEach(apply); else apply(m);
814
+ });
815
+
816
+ // Trigger a render update
817
+ if (this.viewer?.render) {
818
+ this.viewer.render();
819
+ }
820
+ } catch { }
821
+ }
822
+
823
+ #checkCameraChange() {
824
+ // Check if camera has changed and refresh overlays if needed
825
+ try {
826
+ const camera = this.viewer?.camera;
827
+ if (!camera) return;
828
+
829
+ // Get current camera state
830
+ const currentState = {
831
+ px: camera.position.x, py: camera.position.y, pz: camera.position.z,
832
+ rx: camera.rotation.x, ry: camera.rotation.y, rz: camera.rotation.z,
833
+ zoom: camera.zoom || 1
834
+ };
835
+
836
+ // Compare with previous state
837
+ if (!this._lastCameraState) {
838
+ this._lastCameraState = currentState;
839
+ return;
840
+ }
841
+
842
+ const prev = this._lastCameraState;
843
+ const threshold = 0.0001; // Small threshold for floating point comparison
844
+
845
+ const changed =
846
+ Math.abs(currentState.px - prev.px) > threshold ||
847
+ Math.abs(currentState.py - prev.py) > threshold ||
848
+ Math.abs(currentState.pz - prev.pz) > threshold ||
849
+ Math.abs(currentState.rx - prev.rx) > threshold ||
850
+ Math.abs(currentState.ry - prev.ry) > threshold ||
851
+ Math.abs(currentState.rz - prev.rz) > threshold ||
852
+ Math.abs(currentState.zoom - prev.zoom) > threshold;
853
+
854
+ if (changed) {
855
+ this._lastCameraState = currentState;
856
+ this._refreshOverlays();
857
+ }
858
+ } catch (e) {
859
+ console.warn('Error checking camera change:', e);
860
+ }
861
+ }
862
+
863
+ // Apply view-specific transforms from ViewTransform annotations
864
+ #applyViewTransforms() {
865
+ try {
866
+ this.#ensureBaseSolidMatrices();
867
+ // Always return solids to their modeling positions before applying PMI offsets
868
+ this.#resetSolidsToBaseMatrices();
869
+
870
+ const annotationEntities = this._annotationHistory ? this._annotationHistory.getEntries() : [];
871
+ if (!Array.isArray(annotationEntities) || annotationEntities.length === 0) return;
872
+ const activeEntities = annotationEntities.filter((entity) => entity?.enabled !== false);
873
+ if (!activeEntities.length) return;
874
+
875
+ const anns = activeEntities.map((entity) => entity?.inputParams || {});
876
+
877
+
878
+ const handler = annotationRegistry.getSafe?.('viewTransform') || null;
879
+ if (!handler) {
880
+ console.warn('No handler found for viewTransform');
881
+ return;
882
+ }
883
+
884
+ const viewAnns = [];
885
+ for (const ann of anns) {
886
+ if (ann.type === 'viewTransform' || ann.type === 'explodeBody' || ann.type === 'exp') viewAnns.push(ann);
887
+ }
888
+
889
+ const cumulativeState = new Map();
890
+
891
+ for (const ann of viewAnns) {
892
+
893
+ if (typeof handler._resolveSolidReferences === 'function') {
894
+ handler._resolveSolidReferences(ann, this, false);
895
+ }
896
+
897
+ if (typeof handler._ensureOriginalSnapshots === 'function') {
898
+ const solids = Array.isArray(ann.solids) ? ann.solids : [];
899
+ handler._ensureOriginalSnapshots(ann, solids, false, this.viewer);
900
+ }
901
+ }
902
+
903
+ const cloneSnapshot = (snapshot) => {
904
+ if (!snapshot || typeof snapshot !== 'object') return null;
905
+ return {
906
+ position: Array.isArray(snapshot.position) ? snapshot.position.slice() : [0, 0, 0],
907
+ quaternion: Array.isArray(snapshot.quaternion) ? snapshot.quaternion.slice() : [0, 0, 0, 1],
908
+ scale: Array.isArray(snapshot.scale) ? snapshot.scale.slice() : [1, 1, 1],
909
+ worldPosition: Array.isArray(snapshot.worldPosition) ? snapshot.worldPosition.slice() : null,
910
+ };
911
+ };
912
+
913
+ for (const ann of viewAnns) {
914
+ const solids = Array.isArray(ann.solids) ? ann.solids : [];
915
+ if (!solids.length) {
916
+ if (typeof handler.applyTransformsToSolids === 'function') {
917
+ handler.applyTransformsToSolids(ann, this, { startSnapshots: new Map(), cumulativeState });
918
+ }
919
+ continue;
920
+ }
921
+
922
+ let startSnapshots = null;
923
+ if (typeof handler.getOriginalSnapshotMap === 'function') {
924
+ const origMap = handler.getOriginalSnapshotMap(ann);
925
+ startSnapshots = new Map();
926
+ for (const solid of solids) {
927
+ if (!solid || !solid.uuid) continue;
928
+ if (cumulativeState.has(solid.uuid)) {
929
+ const snap = cloneSnapshot(cumulativeState.get(solid.uuid));
930
+ if (snap) startSnapshots.set(solid.uuid, snap);
931
+ } else if (origMap && origMap.has(solid.uuid)) {
932
+ const snap = cloneSnapshot(origMap.get(solid.uuid));
933
+ if (snap) startSnapshots.set(solid.uuid, snap);
934
+ }
935
+ }
936
+ }
937
+
938
+ if (typeof handler.applyTransformsToSolids === 'function') {
939
+ handler.applyTransformsToSolids(ann, this, { startSnapshots, cumulativeState });
940
+ } else if (typeof handler._applyTransformsToSolids === 'function') {
941
+ handler._applyTransformsToSolids(ann, this);
942
+ }
943
+ }
944
+
945
+ // Trigger a render to show the transformed objects
946
+ if (this.viewer?.render) {
947
+ this.viewer.render();
948
+ }
949
+ } catch (error) {
950
+ console.warn('Failed to apply view transforms:', error);
951
+ }
952
+ }
953
+
954
+ // Restore original transforms for all ViewTransform annotations
955
+ #restoreViewTransforms() {
956
+ try {
957
+ const entities = this._annotationHistory ? this._annotationHistory.getEntries() : [];
958
+ if (!Array.isArray(entities) || entities.length === 0) return;
959
+
960
+ const handler = annotationRegistry.getSafe?.('viewTransform') || null;
961
+ if (!handler) return;
962
+
963
+ for (const entity of entities) {
964
+ if (entity?.enabled === false) continue;
965
+ const ann = entity?.inputParams;
966
+ if (!ann || (ann.type !== 'viewTransform' && ann.type !== 'explodeBody' && ann.type !== 'exp')) continue;
967
+
968
+ if (typeof handler.restoreOriginalTransforms === 'function') {
969
+ handler.restoreOriginalTransforms(ann, this);
970
+ }
971
+ }
972
+
973
+ // Trigger a render to show the restored objects
974
+ if (this.viewer?.render) {
975
+ this.viewer.render();
976
+ }
977
+ } catch (error) {
978
+ console.warn('Failed to restore view transforms:', error);
979
+ }
980
+ }
981
+
982
+ #ensureBaseSolidMatrices() {
983
+ if (this._hasBaseMatrices) return;
984
+ try {
985
+ const scene = this.viewer?.scene;
986
+ if (!scene || typeof scene.traverse !== 'function') return;
987
+ const sessionKey = this._baseMatrixSessionKey;
988
+ scene.traverse((obj) => {
989
+ if (!obj || !obj.isObject3D || obj.type !== 'SOLID') return;
990
+ const data = obj.userData || (obj.userData = {});
991
+ try { obj.updateMatrixWorld(true); } catch { }
992
+ const matrix = data.__pmiBaseMatrix;
993
+ if (matrix && typeof matrix.copy === 'function' && matrix.isMatrix4) {
994
+ matrix.copy(obj.matrix);
995
+ } else {
996
+ data.__pmiBaseMatrix = obj.matrix.clone();
997
+ }
998
+ data.__pmiBaseMatrixSession = sessionKey;
999
+ });
1000
+ this._hasBaseMatrices = true;
1001
+ } catch (error) {
1002
+ console.warn('Failed to record base matrices for PMI mode:', error);
1003
+ }
1004
+ }
1005
+
1006
+ #resetSolidsToBaseMatrices() {
1007
+ if (!this._hasBaseMatrices) return;
1008
+ try {
1009
+ const scene = this.viewer?.scene;
1010
+ if (!scene || typeof scene.traverse !== 'function') return;
1011
+ const sessionKey = this._baseMatrixSessionKey;
1012
+ scene.traverse((obj) => {
1013
+ if (!obj || !obj.isObject3D || obj.type !== 'SOLID') return;
1014
+ const data = obj.userData;
1015
+ const base = data?.__pmiBaseMatrix;
1016
+ if (!base || !base.isMatrix4 || data.__pmiBaseMatrixSession !== sessionKey) return;
1017
+ try {
1018
+ obj.matrix.copy(base);
1019
+ obj.matrix.decompose(obj.position, obj.quaternion, obj.scale);
1020
+ if (obj.matrixAutoUpdate) {
1021
+ obj.updateMatrix();
1022
+ }
1023
+ obj.updateMatrixWorld(true);
1024
+ } catch { /* ignore per-object restore errors */ }
1025
+ });
1026
+ try { this.viewer?.render?.(); } catch { }
1027
+ } catch (error) {
1028
+ console.warn('Failed to reset solids to PMI base matrices:', error);
1029
+ }
1030
+ }
1031
+
1032
+ #clearBaseSolidMatrices() {
1033
+ if (!this._hasBaseMatrices) {
1034
+ this._baseMatrixSessionKey = `pmi-base-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1035
+ return;
1036
+ }
1037
+ try {
1038
+ const scene = this.viewer?.scene;
1039
+ if (!scene || typeof scene.traverse !== 'function') return;
1040
+ const sessionKey = this._baseMatrixSessionKey;
1041
+ scene.traverse((obj) => {
1042
+ if (!obj || !obj.isObject3D || obj.type !== 'SOLID') return;
1043
+ const data = obj.userData;
1044
+ if (!data || data.__pmiBaseMatrixSession !== sessionKey) return;
1045
+ delete data.__pmiBaseMatrix;
1046
+ delete data.__pmiBaseMatrixSession;
1047
+ });
1048
+ } catch (error) {
1049
+ console.warn('Failed to clear PMI base matrices:', error);
1050
+ } finally {
1051
+ this._hasBaseMatrices = false;
1052
+ this._baseMatrixSessionKey = `pmi-base-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1053
+ }
1054
+ }
1055
+
1056
+ // --- Annotation 3D visuals ---
1057
+ #clearAnnGroup() {
1058
+ try {
1059
+ if (!this._annGroup) return;
1060
+ for (let i = this._annGroup.children.length - 1; i >= 0; i--) {
1061
+ const c = this._annGroup.children[i];
1062
+ this._annGroup.remove(c);
1063
+ if (c.geometry) c.geometry.dispose?.();
1064
+ if (c.material) c.material.dispose?.();
1065
+ }
1066
+ } catch { }
1067
+ }
1068
+
1069
+ _refreshOverlays() {
1070
+ // Rebuild overlays on camera changes (simpler and type-agnostic)
1071
+ if (this._refreshPending) return;
1072
+ this._refreshPending = true;
1073
+ requestAnimationFrame(() => {
1074
+ this._refreshPending = false;
1075
+ try { this.#rebuildAnnotationObjects(); } catch { }
1076
+ });
1077
+ }
1078
+
1079
+
1080
+
1081
+
1082
+ #markAnnotationsDirty() {
1083
+ this._annotationsDirty = true;
1084
+ // Immediately rebuild annotations instead of waiting for timer
1085
+ try {
1086
+ this.#rebuildAnnotationObjects();
1087
+ this._annotationsDirty = false;
1088
+ this.#_persistView();
1089
+ } catch (error) {
1090
+ console.warn('Failed to rebuild annotations:', error);
1091
+ }
1092
+ }
1093
+
1094
+ // Public: allow external handlers to refresh the side list and 3D objects
1095
+ refreshAnnotationsUI() {
1096
+ try { this._annotationWidget?.render(); } catch { }
1097
+ try {
1098
+ this.#rebuildAnnotationObjects();
1099
+ this._annotationsDirty = false;
1100
+ this.#_persistView();
1101
+ } catch { }
1102
+ }
1103
+
1104
+ #rebuildAnnotationObjects() {
1105
+ this.#clearAnnGroup();
1106
+ const group = this._annGroup;
1107
+ if (!group) return;
1108
+ try { this._labelOverlay?.clear?.(); } catch { }
1109
+ // Ensure overlay exists before we start populating labels
1110
+ const entities = this._annotationHistory ? this._annotationHistory.getEntries() : [];
1111
+ const ctx = {
1112
+ pmimode: this,
1113
+ screenSizeWorld: (px) => { try { return this.#_screenSizeWorld(px); } catch { return 0; } },
1114
+ alignNormal: (alignment, ann) => { try { return this.#_alignNormal(alignment, ann); } catch { return new THREE.Vector3(0, 0, 1); } },
1115
+ updateLabel: (idx, text, worldPos, ann) => { try { this._labelOverlay?.updateLabel?.(idx, text, worldPos, ann); } catch { } },
1116
+ formatReferenceLabel: (ann, text) => { try { return this.#formatReferenceLabel(ann, text); } catch { return text; } },
1117
+ // keep only generic helpers
1118
+ // specific drawing/measuring handled by annotation handlers now
1119
+ };
1120
+ this.__explodeTraceState = new Map();
1121
+ entities.forEach((entity, i) => {
1122
+ try {
1123
+ if (entity?.enabled === false) return;
1124
+ if (!entity || typeof entity.run !== 'function') return;
1125
+ if (!entity.persistentData || typeof entity.persistentData !== 'object') {
1126
+ entity.setPersistentData({});
1127
+ }
1128
+ const renderingContext = {
1129
+ pmimode: this,
1130
+ group,
1131
+ idx: i,
1132
+ ctx,
1133
+ };
1134
+ const runResult = entity.run(renderingContext);
1135
+ if (runResult && typeof runResult.then === 'function') {
1136
+ runResult.catch(() => {});
1137
+ }
1138
+ } catch { }
1139
+ });
1140
+ try { this.viewer.render(); } catch { }
1141
+ // No post-check necessary
1142
+ }
1143
+
1144
+ // Wrap label text in parentheses when marked as a reference dimension
1145
+ #formatReferenceLabel(ann, text) {
1146
+ try {
1147
+ const t = String(text ?? '');
1148
+ if (!t) return t;
1149
+ if (ann && (ann.isReference === true)) return `(${t})`;
1150
+ return t;
1151
+ } catch { return text; }
1152
+ }
1153
+
1154
+
1155
+
1156
+
1157
+
1158
+ #focusAnnotationDialog(idx, ann, e) {
1159
+ e.preventDefault(); e.stopImmediatePropagation?.(); e.stopPropagation();
1160
+
1161
+ try {
1162
+ const entries = this._annotationHistory ? this._annotationHistory.getEntries() : [];
1163
+ const entity = entries[idx];
1164
+ if (!entity) return;
1165
+ const entryId = entity.inputParams?.id || entity.id || idx;
1166
+ this._annotationWidget?.render();
1167
+ requestAnimationFrame(() => {
1168
+ try {
1169
+ const form = this._annotationWidget?.getFormForEntry(String(entryId));
1170
+ const host = form?.uiElement;
1171
+ if (!host) return;
1172
+ const root = host.shadowRoot || host;
1173
+ const row = root?.querySelector('[data-key="text"]');
1174
+ const textField = row ? row.querySelector('textarea, input[type="text"], input') : null;
1175
+ if (textField) {
1176
+ textField.focus();
1177
+ textField.select?.();
1178
+ }
1179
+ } catch (error) {
1180
+ console.warn('Could not focus annotation dialog text field:', error);
1181
+ }
1182
+ });
1183
+ } catch (error) {
1184
+ console.warn('Failed to focus annotation dialog:', error);
1185
+ }
1186
+ }
1187
+
1188
+
1189
+
1190
+
1191
+
1192
+ _handlePointerDown(e) {
1193
+ // Only left-clicks
1194
+ if (e.button !== 0) return;
1195
+ // Avoid interfering if clicking overlays
1196
+ try {
1197
+ const path = e.composedPath?.() || [];
1198
+ if (path.some((el) => el === this._uiTopRight || (el?.classList?.contains?.('pmi-side')))) return;
1199
+ } catch { }
1200
+
1201
+ // If a feature reference_selection is active, let selection widget handle it
1202
+ try { const activeRef = document.querySelector('[active-reference-selection="true"],[active-reference-selection=true]'); if (activeRef) return; } catch { }
1203
+
1204
+ return;
1205
+ }
1206
+
1207
+ #handleLabelClick(idx, ann, e) {
1208
+ try { e.preventDefault(); e.stopImmediatePropagation?.(); e.stopPropagation(); } catch { }
1209
+ this.#collapseAnnotationsToIndex(idx);
1210
+ }
1211
+
1212
+ #handleLabelDragEnd(idx, ann, e) {
1213
+ // Expand the dialog associated with the label that was dragged
1214
+ this.#collapseAnnotationsToIndex(idx);
1215
+ }
1216
+
1217
+ #collapseAnnotationsToIndex(targetIdx) {
1218
+ const entries = this._annotationHistory ? this._annotationHistory.getEntries() : [];
1219
+ if (!entries.length) return;
1220
+ if (!Number.isInteger(targetIdx) || targetIdx < 0 || targetIdx >= entries.length) return;
1221
+ let changed = false;
1222
+ entries.forEach((entry, i) => {
1223
+ const shouldOpen = i === targetIdx;
1224
+ if (!entry.runtimeAttributes || typeof entry.runtimeAttributes !== 'object') entry.runtimeAttributes = {};
1225
+ if (entry.runtimeAttributes.__open !== shouldOpen) {
1226
+ entry.runtimeAttributes.__open = shouldOpen;
1227
+ changed = true;
1228
+ }
1229
+ if (entry.inputParams && typeof entry.inputParams === 'object') {
1230
+ entry.inputParams.__open = shouldOpen;
1231
+ }
1232
+ });
1233
+ if (!changed) return;
1234
+ const targetEntry = entries[targetIdx];
1235
+ const targetId = targetEntry ? (targetEntry.inputParams?.id || targetEntry.id || targetIdx) : targetIdx;
1236
+ this._annotationWidget?.render();
1237
+ requestAnimationFrame(() => {
1238
+ try {
1239
+ const root = this._annotationWidget?._shadow;
1240
+ if (!root) return;
1241
+ const selector = `[data-entry-id="${cssEscape(String(targetId))}"]`;
1242
+ const item = root.querySelector(selector);
1243
+ if (item && typeof item.scrollIntoView === 'function') item.scrollIntoView({ block: 'nearest' });
1244
+ } catch { }
1245
+ });
1246
+ }
1247
+
1248
+
1249
+
1250
+ #startLabelDrag(idx, ann, e) {
1251
+ e.preventDefault(); e.stopImmediatePropagation?.(); e.stopPropagation();
1252
+ try { if (this.viewer?.controls) this.viewer.controls.enabled = false; } catch { }
1253
+ const v = this.viewer; const cam = v?.camera; if (!cam) return;
1254
+
1255
+ try {
1256
+ const handler = annotationRegistry.getSafe?.(ann?.type) || null;
1257
+ if (handler && typeof handler.onLabelPointerDown === "function") {
1258
+ const ctx = {
1259
+ pmimode: this,
1260
+ screenSizeWorld: (px) => { try { return this.#_screenSizeWorld(px); } catch { return 0; } },
1261
+ alignNormal: (alignment, a) => { try { return this.#_alignNormal(alignment, a); } catch { return new THREE.Vector3(0, 0, 1); } },
1262
+ updateLabel: (i, text, worldPos, a) => { try { this._labelOverlay?.updateLabel?.(i, text, worldPos, a); } catch { } },
1263
+ intersectPlane: (ray, plane, out) => { try { return this.#_intersectPlaneBothSides(ray, plane, out); } catch { return null; } },
1264
+ raycastFromEvent: (ev) => {
1265
+ const rect = v.renderer.domElement.getBoundingClientRect();
1266
+ const ndc = new THREE.Vector2(((ev.clientX - rect.left) / rect.width) * 2 - 1, -(((ev.clientY - rect.top) / rect.height) * 2 - 1));
1267
+ v.raycaster.setFromCamera(ndc, cam);
1268
+ return v.raycaster.ray;
1269
+ },
1270
+ };
1271
+ handler.onLabelPointerDown(this, idx, ann, e, ctx);
1272
+ return;
1273
+ }
1274
+ } catch { }
1275
+
1276
+ try { if (this.viewer?.controls) this.viewer.controls.enabled = true; } catch { }
1277
+ }
1278
+
1279
+ // Debug helper: visualize the plane used during label drags
1280
+ showDragPlaneHelper(plane) {
1281
+ try {
1282
+ if (!plane || !plane.normal) return;
1283
+ this.hideDragPlaneHelper();
1284
+ const scene = this.viewer?.scene;
1285
+ if (!scene) return;
1286
+ const size = this.#estimateSceneSpan();
1287
+ const divisions = Math.max(6, Math.min(60, Math.round(size)));
1288
+ const grid = new THREE.GridHelper(size, divisions, 0x00ffff, 0x00ffff);
1289
+ const mats = Array.isArray(grid.material) ? grid.material : [grid.material];
1290
+ mats.forEach((m) => { if (m) { m.transparent = true; m.opacity = 0.35; m.depthWrite = false; } });
1291
+ const up = new THREE.Vector3(0, 1, 0);
1292
+ const n = plane.normal.clone().normalize();
1293
+ if (n.lengthSq() < 1e-12) return;
1294
+ const q = new THREE.Quaternion().setFromUnitVectors(up, n);
1295
+ grid.quaternion.copy(q);
1296
+ const center = plane.coplanarPoint(new THREE.Vector3());
1297
+ grid.position.copy(center);
1298
+ grid.renderOrder = 9998;
1299
+ grid.name = '__PMI_DRAG_PLANE__';
1300
+ scene.add(grid);
1301
+ this._dragPlaneHelper = grid;
1302
+ } catch { }
1303
+ }
1304
+
1305
+ hideDragPlaneHelper() {
1306
+ try {
1307
+ const g = this._dragPlaneHelper;
1308
+ if (g && g.parent) g.parent.remove(g);
1309
+ if (g?.geometry) g.geometry.dispose?.();
1310
+ if (g?.material) {
1311
+ const mats = Array.isArray(g.material) ? g.material : [g.material];
1312
+ mats.forEach((m) => m?.dispose?.());
1313
+ }
1314
+ } catch { }
1315
+ this._dragPlaneHelper = null;
1316
+ }
1317
+
1318
+ #estimateSceneSpan() {
1319
+ try {
1320
+ const scene = this.viewer?.partHistory?.scene || this.viewer?.scene;
1321
+ if (scene) {
1322
+ const box = new THREE.Box3().setFromObject(scene);
1323
+ if (!box.isEmpty()) {
1324
+ const size = box.getSize(new THREE.Vector3());
1325
+ const span = Math.max(size.x, size.y, size.z);
1326
+ if (Number.isFinite(span) && span > 0) return Math.min(Math.max(span * 1.25, 1), 2000);
1327
+ }
1328
+ }
1329
+ } catch { }
1330
+ return 20;
1331
+ }
1332
+
1333
+ // Allow ray-plane intersection even when the plane is "behind" the ray origin
1334
+ // by retrying with a flipped ray direction (infinite line cast).
1335
+ #_intersectPlaneBothSides(ray, plane, out = new THREE.Vector3()) {
1336
+ try {
1337
+ if (!ray || !plane) return null;
1338
+ const direct = ray.intersectPlane(plane, out);
1339
+ if (direct) return out;
1340
+ const invRay = new THREE.Ray(ray.origin.clone(), ray.direction.clone().negate());
1341
+ return invRay.intersectPlane(plane, out);
1342
+ } catch { return null; }
1343
+ }
1344
+
1345
+
1346
+ // Convert legacy annotation shapes to anchor-based ones (in-memory only)
1347
+ #normalizeAnnotation(a) {
1348
+ try {
1349
+ if (!a || typeof a !== 'object') return a;
1350
+ if (a.type === 'leader') {
1351
+ if (!a.anchor && a.start) {
1352
+ // Attempt to map start to nearest edge point fraction
1353
+ const near = this.#nearestEdgeAnchor(new THREE.Vector3(a.start.x || 0, a.start.y || 0, a.start.z || 0));
1354
+ if (near) a.anchor = near;
1355
+ }
1356
+ } else if (a.type === 'dim') {
1357
+ if (!a.a && a.p0) {
1358
+ const near = this.#nearestEdgeAnchor(new THREE.Vector3(a.p0.x || 0, a.p0.y || 0, a.p0.z || 0));
1359
+ if (near) a.a = near;
1360
+ }
1361
+ if (!a.b && a.p1) {
1362
+ const near = this.#nearestEdgeAnchor(new THREE.Vector3(a.p1.x || 0, a.p1.y || 0, a.p1.z || 0));
1363
+ if (near) a.b = near;
1364
+ }
1365
+ } else if (a.type === 'linear') {
1366
+ if ((!Array.isArray(a.targets) || a.targets.length === 0) && (a.aRefName || a.bRefName)) {
1367
+ a.targets = [a.aRefName, a.bRefName].filter(Boolean);
1368
+ }
1369
+ } else if (a.type === 'angle') {
1370
+ if ((!Array.isArray(a.targets) || a.targets.length === 0) && (a.elementARefName || a.elementBRefName)) {
1371
+ a.targets = [a.elementARefName, a.elementBRefName].filter(Boolean);
1372
+ }
1373
+ }
1374
+ } catch { }
1375
+ return a;
1376
+ }
1377
+
1378
+ // Return an anchor ref near a world point by scanning edges/vertices
1379
+ #nearestEdgeAnchor(world) {
1380
+ try {
1381
+ const v = this.viewer;
1382
+ const edges = [];
1383
+ v.scene.traverse((obj) => { if (obj && obj.type === 'EDGE' && obj.visible !== false) edges.push(obj); });
1384
+ let best = null, bestD = Infinity;
1385
+ for (const e of edges) {
1386
+ const info = this.#edgeFractionAtWorld(e, world);
1387
+ if (info && info.dist < bestD) { best = { type: 'edge', edgeId: e.id, edgeName: e.name || null, solidName: e.parent?.name || null, t: info.t }; bestD = info.dist; }
1388
+ }
1389
+ if (best) return best;
1390
+ } catch { }
1391
+ return null;
1392
+ }
1393
+
1394
+ // Hit-test for anchor: prefer VERTEX, else EDGE at fraction along length
1395
+ #pickAnchor(e) {
1396
+ const v = this.viewer;
1397
+ if (!v) return null;
1398
+ // First, try vertices via raycast
1399
+ try {
1400
+ const rect = v.renderer.domElement.getBoundingClientRect();
1401
+ const ndc = new THREE.Vector2(
1402
+ ((e.clientX - rect.left) / rect.width) * 2 - 1,
1403
+ -(((e.clientY - rect.top) / rect.height) * 2 - 1),
1404
+ );
1405
+ v.raycaster.setFromCamera(ndc, v.camera);
1406
+ // include Points children; we'll walk up to VERTEX parents
1407
+ const targets = [];
1408
+ v.scene.traverse((obj) => { if (obj && (obj.type === 'VERTEX' || obj.isPoints) && obj.visible !== false) targets.push(obj); });
1409
+ const hits = targets.length ? v.raycaster.intersectObjects(targets, true) : [];
1410
+ if (hits && hits.length) {
1411
+ let obj = hits[0].object;
1412
+ while (obj && obj.type !== 'VERTEX' && obj.parent) obj = obj.parent;
1413
+ if (obj && obj.type === 'VERTEX') {
1414
+ const w = obj.getWorldPosition(new THREE.Vector3());
1415
+ return { anchor: { type: 'vertex', vertexId: obj.id, name: obj.name || null, solidName: obj.parent?.name || null }, world: w };
1416
+ }
1417
+ }
1418
+ } catch { }
1419
+
1420
+ // Next, try edges using a generous Line/Line2 threshold
1421
+ try {
1422
+ const rect = v.renderer.domElement.getBoundingClientRect();
1423
+ const ndc = new THREE.Vector2(
1424
+ ((e.clientX - rect.left) / rect.width) * 2 - 1,
1425
+ -(((e.clientY - rect.top) / rect.height) * 2 - 1),
1426
+ );
1427
+ v.raycaster.setFromCamera(ndc, v.camera);
1428
+ try {
1429
+ const { width, height } = { width: rect.width, height: rect.height };
1430
+ const wpp = this.#worldPerPixel(v.camera, width, height);
1431
+ v.raycaster.params.Line = v.raycaster.params.Line || {};
1432
+ v.raycaster.params.Line.threshold = Math.max(0.05, wpp * 6);
1433
+ const dpr = (window.devicePixelRatio || 1);
1434
+ v.raycaster.params.Line2 = v.raycaster.params.Line2 || {};
1435
+ v.raycaster.params.Line2.threshold = Math.max(1, 2 * dpr);
1436
+ } catch { }
1437
+ const edges = [];
1438
+ v.scene.traverse((obj) => { if (obj && obj.type === 'EDGE' && obj.visible !== false) edges.push(obj); });
1439
+ const hits = edges.length ? v.raycaster.intersectObjects(edges, true) : [];
1440
+ if (hits && hits.length) {
1441
+ const hit = hits[0];
1442
+ const edge = hit.object;
1443
+ const info = this.#edgeFractionAtWorld(edge, hit.point);
1444
+ if (info) {
1445
+ return { anchor: { type: 'edge', edgeId: edge.id, edgeName: edge.name || null, solidName: edge.parent?.name || null, t: info.t }, world: info.point };
1446
+ }
1447
+ }
1448
+ } catch { }
1449
+ return null;
1450
+ }
1451
+
1452
+ // Compute closest fraction t along EDGE polyline to a world point
1453
+ #edgeFractionAtWorld(edge, worldPoint) {
1454
+ try {
1455
+ const pts = edge.points(true);
1456
+ if (!Array.isArray(pts) || pts.length < 2) return null;
1457
+ const a = new THREE.Vector3(), b = new THREE.Vector3(), p = worldPoint.clone();
1458
+ let total = 0, best = { t: 0, dist: Infinity, point: pts[0] };
1459
+ let accum = 0;
1460
+ for (let i = 0; i < pts.length - 1; i++) {
1461
+ a.set(pts[i].x, pts[i].y, pts[i].z);
1462
+ b.set(pts[i + 1].x, pts[i + 1].y, pts[i + 1].z);
1463
+ const segLen = a.distanceTo(b) || 1e-12;
1464
+ // project p onto segment ab
1465
+ const ab = b.clone().sub(a);
1466
+ const ap = p.clone().sub(a);
1467
+ let t = ab.dot(ap) / (segLen * segLen);
1468
+ if (t < 0) t = 0; else if (t > 1) t = 1;
1469
+ const q = a.clone().addScaledVector(ab, t);
1470
+ const d = q.distanceTo(p);
1471
+ if (d < best.dist) {
1472
+ const tTotal = (accum + t * segLen);
1473
+ best = { t: tTotal, dist: d, point: { x: q.x, y: q.y, z: q.z } };
1474
+ }
1475
+ accum += segLen; total += segLen;
1476
+ }
1477
+ if (total <= 1e-9) return null;
1478
+ return { t: best.t / total, dist: best.dist, point: best.point };
1479
+ } catch { return null; }
1480
+ }
1481
+
1482
+ // Convert anchor ref to current world position
1483
+ #worldPerPixel(camera, width, height) {
1484
+ try {
1485
+ if (camera && camera.isOrthographicCamera) {
1486
+ const zoom = (typeof camera.zoom === 'number' && camera.zoom > 0) ? camera.zoom : 1;
1487
+ const wppX = (camera.right - camera.left) / (width * zoom);
1488
+ const wppY = (camera.top - camera.bottom) / (height * zoom);
1489
+ return Math.max(Math.abs(wppX), Math.abs(wppY));
1490
+ }
1491
+ const dist = camera.position.length();
1492
+ const fovRad = (camera.fov * Math.PI) / 180;
1493
+ const h = 2 * Math.tan(fovRad / 2) * dist;
1494
+ return h / height;
1495
+ } catch { return 1; }
1496
+ }
1497
+
1498
+ #_screenSizeWorld(pixels) {
1499
+ try {
1500
+ const rect = this.viewer?.renderer?.domElement?.getBoundingClientRect?.() || { width: 800, height: 600 };
1501
+ const wpp = this.#worldPerPixel(this.viewer.camera, rect.width, rect.height);
1502
+ return Math.max(0.0001, wpp * (pixels || 1));
1503
+ } catch { return 0.01; }
1504
+ }
1505
+
1506
+ #_alignNormal(alignment, ann) {
1507
+ // If a face/plane reference is provided, use its world normal
1508
+ try {
1509
+ const name = ann?.planeRefName || '';
1510
+ if (name) {
1511
+ const scene = this.viewer?.partHistory?.scene;
1512
+ const obj = scene?.getObjectByName(name);
1513
+ if (obj) {
1514
+ // Face average normal → world
1515
+ if (obj.type === 'FACE' && typeof obj.getAverageNormal === 'function') {
1516
+ const local = obj.getAverageNormal().clone();
1517
+ const nm = new THREE.Matrix3(); nm.getNormalMatrix(obj.matrixWorld);
1518
+ return local.applyMatrix3(nm).normalize();
1519
+ }
1520
+ // PLANE or any Object3D: attempt to use its Z axis as normal
1521
+ const w = new THREE.Vector3(0, 0, 1);
1522
+ try { obj.updateMatrixWorld(true); w.applyMatrix3(new THREE.Matrix3().getNormalMatrix(obj.matrixWorld)); } catch { }
1523
+ if (w.lengthSq()) return w.normalize();
1524
+ }
1525
+ }
1526
+ } catch { }
1527
+ // Fallback: explicit axis or camera view direction
1528
+ const mode = String(alignment || 'view').toLowerCase();
1529
+ if (mode === 'xy') return new THREE.Vector3(0, 0, 1);
1530
+ if (mode === 'yz') return new THREE.Vector3(1, 0, 0);
1531
+ if (mode === 'zx') return new THREE.Vector3(0, 1, 0);
1532
+ const n = new THREE.Vector3();
1533
+ try { this.viewer?.camera?.getWorldDirection?.(n); } catch { }
1534
+ return n.lengthSq() ? n : new THREE.Vector3(0, 0, 1);
1535
+ }
1536
+ }
1537
+
1538
+ function toHex(value) {
1539
+ const n = Number(value);
1540
+ const safe = Number.isFinite(n) ? n : 0;
1541
+ const hex = `000000${(safe >>> 0).toString(16)}`.slice(-6);
1542
+ return `#${hex}`;
1543
+ }
1544
+
1545
+ function hexToInt(value) {
1546
+ if (typeof value !== 'string') return 0;
1547
+ const str = value.startsWith('#') ? value.slice(1) : value;
1548
+ const n = parseInt(str, 16);
1549
+ return Number.isFinite(n) ? n : 0;
1550
+ }