brep-io-kernel 1.0.0-ci.9

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 (271) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +154 -0
  3. package/dist-kernel/brep-kernel.js +74699 -0
  4. package/package.json +58 -0
  5. package/src/BREP/AssemblyComponent.js +42 -0
  6. package/src/BREP/BREP.js +43 -0
  7. package/src/BREP/BetterSolid.js +805 -0
  8. package/src/BREP/Edge.js +103 -0
  9. package/src/BREP/Extrude.js +403 -0
  10. package/src/BREP/Face.js +187 -0
  11. package/src/BREP/MeshRepairer.js +634 -0
  12. package/src/BREP/OffsetShellSolid.js +614 -0
  13. package/src/BREP/PointCloudWrap.js +302 -0
  14. package/src/BREP/Revolve.js +345 -0
  15. package/src/BREP/SolidMethods/authoring.js +112 -0
  16. package/src/BREP/SolidMethods/booleanOps.js +230 -0
  17. package/src/BREP/SolidMethods/chamfer.js +122 -0
  18. package/src/BREP/SolidMethods/edgeResolution.js +25 -0
  19. package/src/BREP/SolidMethods/fillet.js +792 -0
  20. package/src/BREP/SolidMethods/index.js +72 -0
  21. package/src/BREP/SolidMethods/io.js +105 -0
  22. package/src/BREP/SolidMethods/lifecycle.js +103 -0
  23. package/src/BREP/SolidMethods/manifoldOps.js +375 -0
  24. package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
  25. package/src/BREP/SolidMethods/meshQueries.js +264 -0
  26. package/src/BREP/SolidMethods/metadata.js +106 -0
  27. package/src/BREP/SolidMethods/metrics.js +51 -0
  28. package/src/BREP/SolidMethods/transforms.js +361 -0
  29. package/src/BREP/SolidMethods/visualize.js +508 -0
  30. package/src/BREP/SolidShared.js +26 -0
  31. package/src/BREP/Sweep.js +1596 -0
  32. package/src/BREP/Tube.js +857 -0
  33. package/src/BREP/Vertex.js +43 -0
  34. package/src/BREP/applyBooleanOperation.js +704 -0
  35. package/src/BREP/boundsUtils.js +48 -0
  36. package/src/BREP/chamfer.js +551 -0
  37. package/src/BREP/edgePolylineUtils.js +85 -0
  38. package/src/BREP/fillets/common.js +388 -0
  39. package/src/BREP/fillets/fillet.js +1422 -0
  40. package/src/BREP/fillets/filletGeometry.js +15 -0
  41. package/src/BREP/fillets/inset.js +389 -0
  42. package/src/BREP/fillets/offsetHelper.js +143 -0
  43. package/src/BREP/fillets/outset.js +88 -0
  44. package/src/BREP/helix.js +193 -0
  45. package/src/BREP/meshToBrep.js +234 -0
  46. package/src/BREP/primitives.js +279 -0
  47. package/src/BREP/setupManifold.js +71 -0
  48. package/src/BREP/threadGeometry.js +1120 -0
  49. package/src/BREP/triangleUtils.js +8 -0
  50. package/src/BREP/triangulate.js +608 -0
  51. package/src/FeatureRegistry.js +183 -0
  52. package/src/PartHistory.js +1132 -0
  53. package/src/UI/AccordionWidget.js +292 -0
  54. package/src/UI/CADmaterials.js +850 -0
  55. package/src/UI/EnvMonacoEditor.js +522 -0
  56. package/src/UI/FloatingWindow.js +396 -0
  57. package/src/UI/HistoryWidget.js +457 -0
  58. package/src/UI/MainToolbar.js +131 -0
  59. package/src/UI/ModelLibraryView.js +194 -0
  60. package/src/UI/OrthoCameraIdle.js +206 -0
  61. package/src/UI/PluginsWidget.js +280 -0
  62. package/src/UI/SceneListing.js +606 -0
  63. package/src/UI/SelectionFilter.js +629 -0
  64. package/src/UI/ViewCube.js +389 -0
  65. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
  66. package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
  67. package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
  68. package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
  69. package/src/UI/assembly/constraintFaceUtils.js +115 -0
  70. package/src/UI/assembly/constraintHighlightUtils.js +70 -0
  71. package/src/UI/assembly/constraintLabelUtils.js +31 -0
  72. package/src/UI/assembly/constraintPointUtils.js +64 -0
  73. package/src/UI/assembly/constraintSelectionUtils.js +185 -0
  74. package/src/UI/assembly/constraintStatusUtils.js +142 -0
  75. package/src/UI/componentSelectorModal.js +240 -0
  76. package/src/UI/controls/CombinedTransformControls.js +386 -0
  77. package/src/UI/dialogs.js +351 -0
  78. package/src/UI/expressionsManager.js +100 -0
  79. package/src/UI/featureDialogWidgets/booleanField.js +25 -0
  80. package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
  81. package/src/UI/featureDialogWidgets/buttonField.js +45 -0
  82. package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
  83. package/src/UI/featureDialogWidgets/defaultField.js +23 -0
  84. package/src/UI/featureDialogWidgets/fileField.js +66 -0
  85. package/src/UI/featureDialogWidgets/index.js +34 -0
  86. package/src/UI/featureDialogWidgets/numberField.js +165 -0
  87. package/src/UI/featureDialogWidgets/optionsField.js +33 -0
  88. package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
  89. package/src/UI/featureDialogWidgets/stringField.js +24 -0
  90. package/src/UI/featureDialogWidgets/textareaField.js +28 -0
  91. package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
  92. package/src/UI/featureDialogWidgets/transformField.js +252 -0
  93. package/src/UI/featureDialogWidgets/utils.js +43 -0
  94. package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
  95. package/src/UI/featureDialogs.js +1414 -0
  96. package/src/UI/fileManagerWidget.js +615 -0
  97. package/src/UI/history/HistoryCollectionWidget.js +1294 -0
  98. package/src/UI/history/historyCollectionWidget.css.js +257 -0
  99. package/src/UI/history/historyDisplayInfo.js +133 -0
  100. package/src/UI/mobile.js +28 -0
  101. package/src/UI/objectDump.js +442 -0
  102. package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
  103. package/src/UI/pmi/AnnotationHistory.js +353 -0
  104. package/src/UI/pmi/AnnotationRegistry.js +90 -0
  105. package/src/UI/pmi/BaseAnnotation.js +269 -0
  106. package/src/UI/pmi/LabelOverlay.css +102 -0
  107. package/src/UI/pmi/LabelOverlay.js +191 -0
  108. package/src/UI/pmi/PMIMode.js +1550 -0
  109. package/src/UI/pmi/PMIViewsWidget.js +1098 -0
  110. package/src/UI/pmi/annUtils.js +729 -0
  111. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
  112. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
  113. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
  114. package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
  115. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
  116. package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
  117. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
  118. package/src/UI/pmi/pmiStyle.js +44 -0
  119. package/src/UI/sketcher/SketchMode3D.js +4095 -0
  120. package/src/UI/sketcher/dimensions.js +674 -0
  121. package/src/UI/sketcher/glyphs.js +236 -0
  122. package/src/UI/sketcher/highlights.js +60 -0
  123. package/src/UI/toolbarButtons/aboutButton.js +5 -0
  124. package/src/UI/toolbarButtons/exportButton.js +609 -0
  125. package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
  126. package/src/UI/toolbarButtons/importButton.js +160 -0
  127. package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
  128. package/src/UI/toolbarButtons/metadataButton.js +1063 -0
  129. package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
  130. package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
  131. package/src/UI/toolbarButtons/saveButton.js +99 -0
  132. package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
  133. package/src/UI/toolbarButtons/testsButton.js +26 -0
  134. package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
  135. package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
  136. package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
  137. package/src/UI/triangleDebuggerWindow.js +945 -0
  138. package/src/UI/viewer.js +4228 -0
  139. package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
  140. package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
  141. package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
  142. package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
  143. package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
  144. package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
  145. package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
  146. package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
  147. package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
  148. package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
  149. package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
  150. package/src/core/entities/HistoryCollectionBase.js +72 -0
  151. package/src/core/entities/ListEntityBase.js +109 -0
  152. package/src/core/entities/schemaProcesser.js +121 -0
  153. package/src/exporters/sheetMetalFlatPattern.js +659 -0
  154. package/src/exporters/sheetMetalUnfold.js +862 -0
  155. package/src/exporters/step.js +1135 -0
  156. package/src/exporters/threeMF.js +575 -0
  157. package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
  158. package/src/features/boolean/BooleanFeature.js +94 -0
  159. package/src/features/chamfer/ChamferFeature.js +116 -0
  160. package/src/features/datium/DatiumFeature.js +80 -0
  161. package/src/features/edgeFeatureUtils.js +41 -0
  162. package/src/features/extrude/ExtrudeFeature.js +143 -0
  163. package/src/features/fillet/FilletFeature.js +197 -0
  164. package/src/features/helix/HelixFeature.js +405 -0
  165. package/src/features/hole/HoleFeature.js +1050 -0
  166. package/src/features/hole/screwClearance.js +86 -0
  167. package/src/features/hole/threadDesignationCatalog.js +149 -0
  168. package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
  169. package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
  170. package/src/features/imageToFace/imageEditor.js +1270 -0
  171. package/src/features/imageToFace/traceUtils.js +971 -0
  172. package/src/features/import3dModel/Import3dModelFeature.js +151 -0
  173. package/src/features/loft/LoftFeature.js +605 -0
  174. package/src/features/mirror/MirrorFeature.js +151 -0
  175. package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
  176. package/src/features/offsetShell/OffsetShellFeature.js +89 -0
  177. package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
  178. package/src/features/pattern/PatternFeature.js +275 -0
  179. package/src/features/patternLinear/PatternLinearFeature.js +120 -0
  180. package/src/features/patternRadial/PatternRadialFeature.js +186 -0
  181. package/src/features/plane/PlaneFeature.js +154 -0
  182. package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
  183. package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
  184. package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
  185. package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
  186. package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
  187. package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
  188. package/src/features/remesh/RemeshFeature.js +97 -0
  189. package/src/features/revolve/RevolveFeature.js +111 -0
  190. package/src/features/selectionUtils.js +118 -0
  191. package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
  192. package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
  193. package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
  194. package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
  195. package/src/features/sheetMetal/SheetMetalObject.js +141 -0
  196. package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
  197. package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
  198. package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
  199. package/src/features/sheetMetal/profileUtils.js +25 -0
  200. package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
  201. package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
  202. package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
  203. package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
  204. package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
  205. package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
  206. package/src/features/sheetMetal/sheetMetalTree.js +210 -0
  207. package/src/features/sketch/SketchFeature.js +955 -0
  208. package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
  209. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
  210. package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
  211. package/src/features/spline/SplineEditorSession.js +988 -0
  212. package/src/features/spline/SplineFeature.js +1388 -0
  213. package/src/features/spline/splineUtils.js +218 -0
  214. package/src/features/sweep/SweepFeature.js +110 -0
  215. package/src/features/transform/TransformFeature.js +152 -0
  216. package/src/features/tube/TubeFeature.js +635 -0
  217. package/src/fs.proxy.js +625 -0
  218. package/src/idbStorage.js +254 -0
  219. package/src/index.js +12 -0
  220. package/src/main.js +15 -0
  221. package/src/metadataManager.js +64 -0
  222. package/src/path.proxy.js +277 -0
  223. package/src/plugins/ghLoader.worker.js +151 -0
  224. package/src/plugins/pluginManager.js +286 -0
  225. package/src/pmi/PMIViewsManager.js +134 -0
  226. package/src/services/componentLibrary.js +198 -0
  227. package/src/tests/ConsoleCapture.js +189 -0
  228. package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
  229. package/src/tests/browserTests.js +597 -0
  230. package/src/tests/debugBoolean.js +225 -0
  231. package/src/tests/partFiles/badBoolean.json +957 -0
  232. package/src/tests/partFiles/extrudeTest.json +88 -0
  233. package/src/tests/partFiles/filletFail.json +58 -0
  234. package/src/tests/partFiles/import_TEst.part.part.json +646 -0
  235. package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
  236. package/src/tests/test_boolean_subtract.js +27 -0
  237. package/src/tests/test_chamfer.js +17 -0
  238. package/src/tests/test_extrudeFace.js +24 -0
  239. package/src/tests/test_fillet.js +17 -0
  240. package/src/tests/test_fillet_nonClosed.js +45 -0
  241. package/src/tests/test_filletsMoreDifficult.js +46 -0
  242. package/src/tests/test_history_features_basic.js +149 -0
  243. package/src/tests/test_hole.js +282 -0
  244. package/src/tests/test_mirror.js +16 -0
  245. package/src/tests/test_offsetShellGrouping.js +85 -0
  246. package/src/tests/test_plane.js +4 -0
  247. package/src/tests/test_primitiveCone.js +11 -0
  248. package/src/tests/test_primitiveCube.js +7 -0
  249. package/src/tests/test_primitiveCylinder.js +8 -0
  250. package/src/tests/test_primitivePyramid.js +9 -0
  251. package/src/tests/test_primitiveSphere.js +17 -0
  252. package/src/tests/test_primitiveTorus.js +21 -0
  253. package/src/tests/test_pushFace.js +126 -0
  254. package/src/tests/test_sheetMetalContourFlange.js +125 -0
  255. package/src/tests/test_sheetMetal_features.js +80 -0
  256. package/src/tests/test_sketch_openLoop.js +45 -0
  257. package/src/tests/test_solidMetrics.js +58 -0
  258. package/src/tests/test_stlLoader.js +1889 -0
  259. package/src/tests/test_sweepFace.js +55 -0
  260. package/src/tests/test_tube.js +45 -0
  261. package/src/tests/test_tube_closedLoop.js +67 -0
  262. package/src/tests/tests.js +493 -0
  263. package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
  264. package/src/tools/dialogCapturePageFactory.js +227 -0
  265. package/src/tools/featureDialogCapturePage.js +47 -0
  266. package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
  267. package/src/utils/axisHelpers.js +99 -0
  268. package/src/utils/deepClone.js +69 -0
  269. package/src/utils/geometryTolerance.js +37 -0
  270. package/src/utils/normalizeTypeString.js +8 -0
  271. package/src/utils/xformMath.js +51 -0
@@ -0,0 +1,1098 @@
1
+ // PMIViewsWidget.js
2
+ // ES6, no frameworks. Provides a simple list of saved PMI views
3
+ // (camera snapshots) with capture, rename, apply, and delete.
4
+ // Views are persisted with the PartHistory instance.
5
+
6
+ import * as THREE from 'three';
7
+ import { captureCameraSnapshot, applyCameraSnapshot, adjustOrthographicFrustum } from './annUtils.js';
8
+ import { AnnotationHistory } from './AnnotationHistory.js';
9
+
10
+ const UPDATE_CAMERA_TOOLTIP = 'Update this view to match the current camera';
11
+
12
+ export class PMIViewsWidget {
13
+ constructor(viewer) {
14
+ this.viewer = viewer;
15
+ this.uiElement = document.createElement('div');
16
+ this.uiElement.className = 'pmi-views-root';
17
+ this._ensureStyles();
18
+
19
+ this.views = [];
20
+ this._activeViewIndex = null;
21
+ this._activeMenu = null;
22
+ this._menuOutsideHandler = null;
23
+ this._onHistoryViewsChanged = (views) => {
24
+ this.views = Array.isArray(views) ? views : this._getViewsFromHistory();
25
+ this._renderList();
26
+ };
27
+
28
+ this._buildUI();
29
+ this.refreshFromHistory();
30
+ this._renderList();
31
+
32
+ try {
33
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
34
+ this._removeHistoryListener = manager ? manager.addListener(this._onHistoryViewsChanged) : null;
35
+ } catch {
36
+ this._removeHistoryListener = null;
37
+ }
38
+ }
39
+
40
+ dispose() {
41
+ if (typeof this._removeHistoryListener === 'function') {
42
+ try { this._removeHistoryListener(); } catch {}
43
+ }
44
+ this._removeHistoryListener = null;
45
+ this._closeActiveMenu();
46
+ }
47
+
48
+ refreshFromHistory() {
49
+ this.views = this._getViewsFromHistory();
50
+ }
51
+
52
+ _getViewsFromHistory() {
53
+ try {
54
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
55
+ if (!manager || typeof manager.getViews !== 'function') return [];
56
+ const views = manager.getViews();
57
+ return Array.isArray(views) ? views : [];
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ _getActiveViewIndex() {
64
+ const modeIndex = this.viewer?._pmiMode?.viewIndex;
65
+ if (Number.isInteger(modeIndex) && modeIndex >= 0) return modeIndex;
66
+ if (Number.isInteger(this._activeViewIndex) && this._activeViewIndex >= 0) return this._activeViewIndex;
67
+ return null;
68
+ }
69
+
70
+ _setActiveViewIndex(index) {
71
+ if (Number.isInteger(index) && index >= 0) {
72
+ this._activeViewIndex = index;
73
+ } else {
74
+ this._activeViewIndex = null;
75
+ }
76
+ }
77
+
78
+ _resolveViewName(view, index) {
79
+ const fallback = `View ${index + 1}`;
80
+ if (!view || typeof view !== 'object') return fallback;
81
+ const name = typeof view.viewName === 'string' ? view.viewName : (typeof view.name === 'string' ? view.name : '');
82
+ const trimmed = String(name || '').trim();
83
+ return trimmed || fallback;
84
+ }
85
+
86
+ // ---- UI ----
87
+ _ensureStyles() {
88
+ if (document.getElementById('pmi-views-widget-styles')) return;
89
+ const style = document.createElement('style');
90
+ style.id = 'pmi-views-widget-styles';
91
+ style.textContent = `
92
+ .pmi-views-root { padding: 6px; }
93
+ .pmi-row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; border-bottom: 1px solid #1f2937; background: transparent; transition: background-color .12s ease; position: relative; }
94
+ .pmi-row:hover { background: #0f172a; }
95
+ .pmi-row.header { background: #111827; border-bottom: 1px solid #1f2937; padding-bottom: 8px; margin-bottom: 6px; border-radius: 4px; }
96
+ .pmi-row.active { background: #0f172a; border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,.35); }
97
+ .pmi-grow { flex: 1 1 auto; min-width: 0; }
98
+ .pmi-input { width: 100%; box-sizing: border-box; padding: 6px 8px; background: #0b0e14; color: #e5e7eb; border: 1px solid #374151; border-radius: 8px; outline: none; transition: border-color .15s ease, box-shadow .15s ease; }
99
+ .pmi-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.15); }
100
+ .pmi-btn { background: rgba(255,255,255,.03); color: #f9fafb; border: 1px solid #374151; padding: 4px 8px; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 12px; line-height: 1; height: 26px; display: inline-flex; align-items: center; justify-content: center; transition: border-color .15s ease, background-color .15s ease, transform .05s ease; }
101
+ .pmi-btn.icon { width: 26px; padding: 0; font-size: 16px; }
102
+ .pmi-btn:hover { border-color: #3b82f6; background: rgba(59,130,246,.12); }
103
+ .pmi-btn:active { transform: translateY(1px); }
104
+ .pmi-btn.danger { border-color: #7f1d1d; color: #fecaca; }
105
+ .pmi-btn.danger:hover { border-color: #ef4444; background: rgba(239,68,68,.15); color: #fff; }
106
+ .pmi-list { display: flex; flex-direction: column; gap: 2px; }
107
+ .pmi-name { font-weight: 600; color: #e5e7eb; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
108
+ .pmi-name-btn { background: none; border: none; padding: 0; margin: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; display: block; width: 100%; }
109
+ .pmi-name-btn:hover { color: #93c5fd; }
110
+ .pmi-name-btn:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
111
+ .pmi-row-menu { position: absolute; right: 6px; top: calc(100% + 4px); background: #0b1120; border: 1px solid #1f2937; border-radius: 10px; padding: 8px; display: none; flex-direction: column; gap: 6px; min-width: 180px; box-shadow: 0 12px 24px rgba(0,0,0,.45); z-index: 20; }
112
+ .pmi-row-menu.open { display: flex; }
113
+ .pmi-row-menu .pmi-btn { width: 100%; justify-content: flex-start; }
114
+ .pmi-row-menu .pmi-btn.danger { justify-content: center; }
115
+ .pmi-row-menu-wireframe { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #e5e7eb; }
116
+ .pmi-row-menu hr { border: none; border-top: 1px solid #1f2937; margin: 4px 0; }
117
+ `;
118
+ document.head.appendChild(style);
119
+ }
120
+
121
+ _buildUI() {
122
+ // Header: input for new view name + Capture button
123
+ const header = document.createElement('div');
124
+ header.className = 'pmi-row header';
125
+
126
+ const newViewLabel = document.createElement('div');
127
+ newViewLabel.className = 'pmi-name pmi-grow';
128
+ newViewLabel.textContent = 'New view';
129
+ newViewLabel.title = 'Capture the current camera as a new PMI view';
130
+ header.appendChild(newViewLabel);
131
+
132
+ const capBtn = document.createElement('button');
133
+ capBtn.className = 'pmi-btn';
134
+ capBtn.title = 'Capture current camera as a view';
135
+ capBtn.textContent = 'Capture';
136
+ capBtn.addEventListener('click', () => this._captureCurrent());
137
+ header.appendChild(capBtn);
138
+
139
+ const exportBtn = document.createElement('button');
140
+ exportBtn.className = 'pmi-btn';
141
+ exportBtn.title = 'Export all PMI views as images';
142
+ exportBtn.textContent = 'Export Images';
143
+ exportBtn.addEventListener('click', () => { this._exportImages(); });
144
+ header.appendChild(exportBtn);
145
+
146
+ this.uiElement.appendChild(header);
147
+
148
+ this.listEl = document.createElement('div');
149
+ this.listEl.className = 'pmi-list';
150
+ this.uiElement.appendChild(this.listEl);
151
+ }
152
+
153
+ _renderList() {
154
+ this._closeActiveMenu();
155
+ this.listEl.textContent = '';
156
+ const views = Array.isArray(this.views) ? this.views : [];
157
+ const activeIndex = this._getActiveViewIndex();
158
+ views.forEach((v, idx) => {
159
+ const row = document.createElement('div');
160
+ row.className = 'pmi-row';
161
+ if (idx === activeIndex) {
162
+ row.classList.add('active');
163
+ row.setAttribute('aria-current', 'true');
164
+ }
165
+
166
+ const viewName = this._resolveViewName(v, idx);
167
+ const nameButton = document.createElement('button');
168
+ nameButton.type = 'button';
169
+ nameButton.className = 'pmi-name pmi-name-btn pmi-grow';
170
+ nameButton.textContent = viewName;
171
+ nameButton.title = 'Click to edit annotations for this view';
172
+ nameButton.addEventListener('click', () => {
173
+ this._enterEditMode(v, idx);
174
+ setTimeout(() => this._enterEditMode(v, idx), 200);
175
+ });
176
+ row.appendChild(nameButton);
177
+
178
+ const startRename = () => {
179
+ this._closeActiveMenu();
180
+ if (!row.contains(nameButton)) {
181
+ const existingInput = row.querySelector('input.pmi-input');
182
+ if (existingInput) {
183
+ existingInput.focus();
184
+ existingInput.select?.();
185
+ }
186
+ return;
187
+ }
188
+ const nameInput = document.createElement('input');
189
+ nameInput.type = 'text';
190
+ nameInput.value = viewName;
191
+ nameInput.className = 'pmi-input pmi-grow';
192
+
193
+ let finished = false;
194
+ const finishRename = (commit) => {
195
+ if (finished) return;
196
+ finished = true;
197
+ if (commit) {
198
+ const fallback = viewName;
199
+ const newName = nameInput.value.trim();
200
+ const finalName = newName || fallback;
201
+ if (finalName !== viewName) {
202
+ const updateFn = (entry) => {
203
+ if (!entry || typeof entry !== 'object') return entry;
204
+ entry.viewName = finalName;
205
+ entry.name = finalName;
206
+ return entry;
207
+ };
208
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
209
+ const updated = manager?.updateView?.(idx, updateFn);
210
+ if (!updated) {
211
+ updateFn(v);
212
+ this.refreshFromHistory();
213
+ }
214
+ }
215
+ }
216
+ this._renderList();
217
+ };
218
+
219
+ nameInput.addEventListener('keydown', (evt) => {
220
+ if (evt.key === 'Enter') {
221
+ finishRename(true);
222
+ } else if (evt.key === 'Escape') {
223
+ finishRename(false);
224
+ }
225
+ });
226
+ nameInput.addEventListener('blur', () => finishRename(true));
227
+
228
+ row.replaceChild(nameInput, nameButton);
229
+ nameInput.focus();
230
+ nameInput.select();
231
+ };
232
+
233
+ const deleteView = () => {
234
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
235
+ const removed = manager?.removeView?.(idx);
236
+ if (!removed) {
237
+ this.views.splice(idx, 1);
238
+ this.refreshFromHistory();
239
+ }
240
+ this._renderList();
241
+ };
242
+
243
+ const menuBtn = document.createElement('button');
244
+ menuBtn.type = 'button';
245
+ menuBtn.className = 'pmi-btn icon';
246
+ menuBtn.title = 'View options';
247
+ menuBtn.setAttribute('aria-label', 'View options');
248
+ menuBtn.textContent = '⋯';
249
+
250
+ const menu = document.createElement('div');
251
+ menu.className = 'pmi-row-menu';
252
+
253
+ const makeMenuButton = (label, handler, opts = {}) => {
254
+ const btn = document.createElement('button');
255
+ btn.type = 'button';
256
+ btn.className = `pmi-btn${opts.danger ? ' danger' : ''}`;
257
+ btn.textContent = label;
258
+ if (opts.title) btn.title = opts.title;
259
+ btn.addEventListener('click', (evt) => {
260
+ evt.stopPropagation();
261
+ handler();
262
+ this._closeActiveMenu();
263
+ });
264
+ return btn;
265
+ };
266
+
267
+ menu.appendChild(makeMenuButton('Update Camera', () => this._updateViewCamera(idx), { title: UPDATE_CAMERA_TOOLTIP }));
268
+ menu.appendChild(makeMenuButton('Rename View', startRename));
269
+ menu.appendChild(makeMenuButton('Delete View', deleteView, { danger: true, title: 'Delete this view' }));
270
+ const divider = document.createElement('hr');
271
+ menu.appendChild(divider);
272
+
273
+ const wireframeLabel = document.createElement('label');
274
+ wireframeLabel.className = 'pmi-row-menu-wireframe';
275
+ const wireframeCheckbox = document.createElement('input');
276
+ wireframeCheckbox.type = 'checkbox';
277
+ const storedWireframe = (v.viewSettings || v.settings)?.wireframe;
278
+ wireframeCheckbox.checked = (typeof storedWireframe === 'boolean') ? storedWireframe : false;
279
+ wireframeCheckbox.addEventListener('change', (evt) => {
280
+ evt.stopPropagation();
281
+ this._setViewWireframe(idx, Boolean(wireframeCheckbox.checked));
282
+ });
283
+ const wireframeText = document.createElement('span');
284
+ wireframeText.textContent = 'Wireframe';
285
+ wireframeLabel.appendChild(wireframeCheckbox);
286
+ wireframeLabel.appendChild(wireframeText);
287
+ menu.appendChild(wireframeLabel);
288
+
289
+ menuBtn.addEventListener('click', (evt) => {
290
+ evt.stopPropagation();
291
+ this._toggleRowMenu(menu, menuBtn);
292
+ });
293
+
294
+ row.appendChild(menuBtn);
295
+ row.appendChild(menu);
296
+
297
+ row.addEventListener('dblclick', (e) => {
298
+ const target = e.target;
299
+ const tagName = target?.tagName;
300
+ if (menu.contains(target) || target === menuBtn || tagName === 'INPUT') return;
301
+ this._applyView(v, { index: idx });
302
+ });
303
+
304
+ this.listEl.appendChild(row);
305
+ });
306
+ }
307
+
308
+ _toggleRowMenu(menu, trigger) {
309
+ if (this._activeMenu && this._activeMenu !== menu) {
310
+ this._closeActiveMenu();
311
+ }
312
+ if (menu.classList.contains('open')) {
313
+ this._closeActiveMenu();
314
+ return;
315
+ }
316
+ menu.classList.add('open');
317
+ this._activeMenu = menu;
318
+ this._menuOutsideHandler = (evt) => {
319
+ if (!this._activeMenu) return;
320
+ if (this._activeMenu.contains(evt.target) || trigger.contains(evt.target)) return;
321
+ this._closeActiveMenu();
322
+ };
323
+ setTimeout(() => {
324
+ if (this._menuOutsideHandler) {
325
+ document.addEventListener('mousedown', this._menuOutsideHandler);
326
+ }
327
+ }, 0);
328
+ }
329
+
330
+ _closeActiveMenu() {
331
+ if (this._activeMenu) {
332
+ this._activeMenu.classList.remove('open');
333
+ this._activeMenu = null;
334
+ }
335
+ if (this._menuOutsideHandler) {
336
+ document.removeEventListener('mousedown', this._menuOutsideHandler);
337
+ this._menuOutsideHandler = null;
338
+ }
339
+ }
340
+
341
+ // ---- Actions ----
342
+ async _captureCurrent() {
343
+ try {
344
+ const v = this.viewer;
345
+ const cam = v?.camera;
346
+ if (!cam) return;
347
+ const cameraSnap = captureCameraSnapshot(cam, { controls: this.viewer?.controls });
348
+ if (!cameraSnap) return;
349
+ const fallbackIndex = Array.isArray(this.views) ? this.views.length : 0;
350
+ const defaultName = `View ${fallbackIndex + 1}`;
351
+ const promptFn = (typeof window !== 'undefined' && typeof window.prompt === 'function')
352
+ ? window.prompt.bind(window)
353
+ : (typeof prompt === 'function' ? prompt : null);
354
+ const response = promptFn ? await promptFn('Enter a name for this view', defaultName) : defaultName;
355
+ if (response === null) return; // user cancelled
356
+ const name = String(response || '').trim() || defaultName;
357
+ const snap = {
358
+ viewName: name,
359
+ name,
360
+ camera: cameraSnap,
361
+ // Persist basic view settings (extensible). Currently only wireframe render mode.
362
+ viewSettings: {
363
+ wireframe: this._detectWireframe(v?.scene)
364
+ },
365
+ annotations: [],
366
+ };
367
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
368
+ const added = manager?.addView?.(snap);
369
+ if (!added) {
370
+ this.views.push(snap);
371
+ this.refreshFromHistory();
372
+ }
373
+ const newIndex = Array.isArray(this.views) ? Math.max(0, (this.views.length - 1)) : 0;
374
+ this._setActiveViewIndex(newIndex);
375
+ this._renderList();
376
+ } catch { /* ignore */ }
377
+ }
378
+
379
+ async _exportImages() {
380
+ if (this._exportingImages) return;
381
+ const views = Array.isArray(this.views) ? this.views : [];
382
+ if (!views.length) {
383
+ alert('No PMI views to export.');
384
+ return;
385
+ }
386
+
387
+ const viewer = this.viewer;
388
+ const canvas = viewer?.renderer?.domElement;
389
+ if (!viewer || !canvas) {
390
+ alert('Viewer is not ready to export images.');
391
+ return;
392
+ }
393
+
394
+ const captures = [];
395
+ try {
396
+ await this._withViewCubeHidden(async () => {
397
+ this._exportingImages = true;
398
+ const originalSnapshot = captureCameraSnapshot(viewer.camera, { controls: viewer.controls });
399
+ const originalWireframe = this._detectWireframe(viewer.scene);
400
+ const previousActive = this._getActiveViewIndex();
401
+
402
+ try {
403
+ for (let i = 0; i < views.length; i++) {
404
+ const view = views[i];
405
+ const name = this._resolveViewName(view, i);
406
+ this._applyView(view, { index: i, suppressActive: true });
407
+ const overlay = await this._buildExportAnnotations(view);
408
+ await this._renderAndWait(2);
409
+ const dataUrl = await this._captureCanvasImage(overlay.labels);
410
+ if (!dataUrl) {
411
+ throw new Error(`Failed to capture image for view "${name}"`);
412
+ }
413
+ captures.push({ name, dataUrl });
414
+ try { overlay.cleanup?.(); } catch { }
415
+ }
416
+ } finally {
417
+ this._restoreViewState(originalSnapshot, originalWireframe);
418
+ if (previousActive != null) {
419
+ this._setActiveViewIndex(previousActive);
420
+ this._renderList();
421
+ }
422
+ this._exportingImages = false;
423
+ }
424
+ });
425
+ } catch (err) {
426
+ console.error('PMI export failed:', err);
427
+ alert(`Export failed: ${err?.message || err}`);
428
+ return;
429
+ }
430
+
431
+ if (!captures.length) {
432
+ alert('No images were captured.');
433
+ return;
434
+ }
435
+
436
+ const popup = (typeof window !== 'undefined' && typeof window.open === 'function')
437
+ ? window.open('', '_blank')
438
+ : null;
439
+ if (!popup) {
440
+ alert('Images generated, but pop-ups were blocked. Please allow pop-ups to view them.');
441
+ return;
442
+ }
443
+
444
+ const doc = popup.document;
445
+ doc.title = 'PMI View Images';
446
+ doc.body.textContent = '';
447
+
448
+ this._injectExportStyles(doc);
449
+
450
+ const title = doc.createElement('div');
451
+ title.className = 'pmi-export-title';
452
+ title.textContent = 'PMI View Images';
453
+ doc.body.appendChild(title);
454
+
455
+ const grid = doc.createElement('div');
456
+ grid.className = 'pmi-export-grid';
457
+ doc.body.appendChild(grid);
458
+
459
+ for (const { name, dataUrl } of captures) {
460
+ const card = doc.createElement('div');
461
+ card.className = 'pmi-export-card';
462
+ const img = doc.createElement('img');
463
+ img.src = dataUrl;
464
+ img.alt = name;
465
+ const caption = doc.createElement('div');
466
+ caption.className = 'pmi-export-caption';
467
+ caption.textContent = name;
468
+ card.appendChild(img);
469
+ card.appendChild(caption);
470
+ grid.appendChild(card);
471
+ }
472
+ }
473
+
474
+ // Generate labeled PNGs for all views (for packaging into 3MF). Throws on failure.
475
+ async captureViewImagesForPackage() {
476
+ if (this._exportingImages) throw new Error('PMI view export already in progress');
477
+ const views = Array.isArray(this.views) ? this.views : [];
478
+ if (!views.length) return {};
479
+
480
+ const viewer = this.viewer;
481
+ const canvas = viewer?.renderer?.domElement;
482
+ if (!viewer || !canvas) throw new Error('Viewer is not ready to export images');
483
+
484
+ const captures = [];
485
+ try {
486
+ await this._withViewCubeHidden(async () => {
487
+ this._exportingImages = true;
488
+ const originalSnapshot = captureCameraSnapshot(viewer.camera, { controls: viewer.controls });
489
+ const originalWireframe = this._detectWireframe(viewer.scene);
490
+ const previousActive = this._getActiveViewIndex();
491
+
492
+ try {
493
+ for (let i = 0; i < views.length; i++) {
494
+ const view = views[i];
495
+ const name = this._resolveViewName(view, i);
496
+ this._applyView(view, { index: i, suppressActive: true });
497
+ const overlay = await this._buildExportAnnotations(view);
498
+ await this._renderAndWait(2);
499
+ const dataUrl = await this._captureCanvasImage(overlay.labels);
500
+ if (!dataUrl) {
501
+ throw new Error(`Failed to capture image for view "${name}"`);
502
+ }
503
+ captures.push({ name, dataUrl });
504
+ try { overlay.cleanup?.(); } catch { }
505
+ }
506
+ } finally {
507
+ this._restoreViewState(originalSnapshot, originalWireframe);
508
+ if (previousActive != null) {
509
+ this._setActiveViewIndex(previousActive);
510
+ this._renderList();
511
+ }
512
+ this._exportingImages = false;
513
+ }
514
+ });
515
+ } finally {
516
+ }
517
+
518
+ const files = {};
519
+ captures.forEach(({ name, dataUrl }) => {
520
+ const fileName = `${this._safeFileName(name, 'view')}.png`;
521
+ const path = `views/${fileName}`;
522
+ files[path] = this._dataUrlToUint8Array(dataUrl);
523
+ });
524
+ return files;
525
+ }
526
+
527
+ async _buildExportAnnotations(view) {
528
+ const cleanup = () => {};
529
+ try {
530
+ const viewer = this.viewer;
531
+ const scene = viewer?.partHistory?.scene || viewer?.scene;
532
+ if (!viewer || !scene) return { labels: [], cleanup };
533
+
534
+ const pmimode = {
535
+ viewer,
536
+ _opts: {
537
+ dimDecimals: 3,
538
+ angleDecimals: 1,
539
+ noteText: '',
540
+ leaderText: 'TEXT HERE',
541
+ },
542
+ __explodeTraceState: new Map(),
543
+ };
544
+ const history = new AnnotationHistory(pmimode);
545
+ try { history.load(Array.isArray(view?.annotations) ? view.annotations : []); } catch { }
546
+ const entries = history.getEntries();
547
+ if (!entries.length) return { labels: [], cleanup };
548
+
549
+ const group = new THREE.Group();
550
+ group.name = '__PMI_EXPORT_ANN__';
551
+ group.renderOrder = 9994;
552
+ scene.add(group);
553
+
554
+ const labels = [];
555
+ const ctx = {
556
+ screenSizeWorld: (px) => this._screenSizeWorld(px),
557
+ alignNormal: (alignment, ann) => this._alignNormal(alignment, ann),
558
+ formatReferenceLabel: (ann, text) => this._formatReferenceLabel(ann, text),
559
+ updateLabel: (idx, text, worldPos, ann) => {
560
+ if (!worldPos || text == null) return;
561
+ const world = this._normalizeLabelPosition(worldPos);
562
+ if (!world) return;
563
+ labels[idx] = {
564
+ text: String(text),
565
+ world,
566
+ anchor: ann?.anchorPosition || ann?.alignmentAnchor || null,
567
+ };
568
+ },
569
+ };
570
+
571
+ for (let i = 0; i < entries.length; i++) {
572
+ const entry = entries[i];
573
+ if (!entry || typeof entry.run !== 'function' || entry.enabled === false) continue;
574
+ // eslint-disable-next-line no-await-in-loop
575
+ await entry.run({ pmimode, group, idx: i, ctx });
576
+ }
577
+
578
+ if (entries.length && labels.length === 0) {
579
+ throw new Error('Annotation export produced no labels');
580
+ }
581
+
582
+ const cleanupFn = () => {
583
+ try { scene.remove(group); } catch { }
584
+ };
585
+ return { labels: labels.filter(Boolean), cleanup: cleanupFn };
586
+ } catch (err) {
587
+ throw err;
588
+ }
589
+ }
590
+
591
+ async _captureCanvasImage(labels = []) {
592
+ const canvas = this.viewer?.renderer?.domElement;
593
+ const camera = this.viewer?.camera;
594
+ if (!canvas || !camera) throw new Error('Renderer not ready for capture');
595
+ const width = canvas.width || canvas.clientWidth || 1;
596
+ const height = canvas.height || canvas.clientHeight || 1;
597
+ const baseData = canvas.toDataURL('image/png');
598
+ if (!Array.isArray(labels) || labels.length === 0) return baseData;
599
+
600
+ const cssWidth = canvas.clientWidth || width;
601
+ const cssHeight = canvas.clientHeight || height;
602
+ const svgMarkup = this._composeLabelSVG(baseData, labels, width, height, cssWidth, cssHeight);
603
+ if (!svgMarkup) throw new Error('Failed to compose SVG for labels');
604
+ const svgPng = await this._svgToPngDataUrl(svgMarkup, width, height);
605
+ if (!svgPng) throw new Error('Failed to convert SVG to PNG');
606
+ return svgPng;
607
+ }
608
+
609
+ _resolveLabelAnchorOffsets(anchor) {
610
+ const key = String(anchor || '').toLowerCase();
611
+ if (key === 'left top') return { ox: 1, oy: 0 };
612
+ if (key === 'left middle') return { ox: 1, oy: 0.5 };
613
+ if (key === 'left bottom') return { ox: 1, oy: 1 };
614
+ if (key === 'right top') return { ox: 0, oy: 0 };
615
+ if (key === 'right middle') return { ox: 0, oy: 0.5 };
616
+ if (key === 'right bottom') return { ox: 0, oy: 1 };
617
+ return { ox: 0.5, oy: 0.5 };
618
+ }
619
+
620
+ _projectWorldToScreen(world, camera, viewport) {
621
+ try {
622
+ if (!world || !camera) return null;
623
+ const { width = 1, height = 1 } = viewport || {};
624
+ const v = world.clone ? world.clone() : new THREE.Vector3(world.x || 0, world.y || 0, world.z || 0);
625
+ v.project(camera);
626
+ if (!Number.isFinite(v.x) || !Number.isFinite(v.y)) return null;
627
+ return {
628
+ x: (v.x * 0.5 + 0.5) * width,
629
+ y: (-v.y * 0.5 + 0.5) * height,
630
+ };
631
+ } catch { return null; }
632
+ }
633
+
634
+ _normalizeLabelPosition(worldPos) {
635
+ try {
636
+ if (!worldPos) return null;
637
+ if (worldPos.isVector3) return worldPos.clone();
638
+ if (Array.isArray(worldPos) && worldPos.length >= 3) {
639
+ return new THREE.Vector3(Number(worldPos[0]) || 0, Number(worldPos[1]) || 0, Number(worldPos[2]) || 0);
640
+ }
641
+ if (typeof worldPos === 'object') {
642
+ return new THREE.Vector3(Number(worldPos.x) || 0, Number(worldPos.y) || 0, Number(worldPos.z) || 0);
643
+ }
644
+ return null;
645
+ } catch { return null; }
646
+ }
647
+
648
+ _drawRoundedRect(ctx, x, y, w, h, r = 6, fill = '#0f172a', stroke = '#1f2937') {
649
+ const radius = Math.max(0, Math.min(r, Math.min(w, h) / 2));
650
+ ctx.beginPath();
651
+ ctx.moveTo(x + radius, y);
652
+ ctx.lineTo(x + w - radius, y);
653
+ ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
654
+ ctx.lineTo(x + w, y + h - radius);
655
+ ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
656
+ ctx.lineTo(x + radius, y + h);
657
+ ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
658
+ ctx.lineTo(x, y + radius);
659
+ ctx.quadraticCurveTo(x, y, x + radius, y);
660
+ ctx.closePath();
661
+ ctx.fillStyle = fill;
662
+ ctx.fill();
663
+ ctx.strokeStyle = stroke;
664
+ ctx.lineWidth = 1;
665
+ ctx.stroke();
666
+ }
667
+
668
+ _screenSizeWorld(pixels = 1) {
669
+ try {
670
+ const canvasRect = this.viewer?.renderer?.domElement?.getBoundingClientRect?.() || { width: 800, height: 600 };
671
+ const wpp = this._worldPerPixel(this.viewer?.camera, canvasRect.width, canvasRect.height);
672
+ return Math.max(0.0001, wpp * (pixels || 1));
673
+ } catch { return 0.01; }
674
+ }
675
+
676
+ _worldPerPixel(camera, width, height) {
677
+ try {
678
+ if (camera && camera.isOrthographicCamera) {
679
+ const zoom = (typeof camera.zoom === 'number' && camera.zoom > 0) ? camera.zoom : 1;
680
+ const safeW = width || 1;
681
+ const safeH = height || 1;
682
+ const wppX = (camera.right - camera.left) / (safeW * zoom);
683
+ const wppY = (camera.top - camera.bottom) / (safeH * zoom);
684
+ return Math.max(Math.abs(wppX), Math.abs(wppY));
685
+ }
686
+ const dist = camera?.position?.length?.() || 1;
687
+ const fovRad = (camera?.fov || 60) * Math.PI / 180;
688
+ const h = 2 * Math.tan(fovRad / 2) * dist;
689
+ return h / (height || 1);
690
+ } catch { return 1; }
691
+ }
692
+
693
+ _alignNormal(alignment, ann) {
694
+ try {
695
+ const name = ann?.planeRefName || ann?.planeRef || '';
696
+ if (name) {
697
+ const scene = this.viewer?.partHistory?.scene;
698
+ const obj = scene?.getObjectByName(name);
699
+ if (obj) {
700
+ if (obj.type === 'FACE' && typeof obj.getAverageNormal === 'function') {
701
+ const local = obj.getAverageNormal().clone();
702
+ const nm = new THREE.Matrix3(); nm.getNormalMatrix(obj.matrixWorld);
703
+ return local.applyMatrix3(nm).normalize();
704
+ }
705
+ const w = new THREE.Vector3(0, 0, 1);
706
+ try { obj.updateMatrixWorld(true); w.applyMatrix3(new THREE.Matrix3().getNormalMatrix(obj.matrixWorld)); } catch { }
707
+ if (w.lengthSq()) return w.normalize();
708
+ }
709
+ }
710
+ } catch { /* ignore */ }
711
+ const mode = String(alignment || 'view').toLowerCase();
712
+ if (mode === 'xy') return new THREE.Vector3(0, 0, 1);
713
+ if (mode === 'yz') return new THREE.Vector3(1, 0, 0);
714
+ if (mode === 'zx') return new THREE.Vector3(0, 1, 0);
715
+ const n = new THREE.Vector3();
716
+ try { this.viewer?.camera?.getWorldDirection?.(n); } catch { }
717
+ return n.lengthSq() ? n : new THREE.Vector3(0, 0, 1);
718
+ }
719
+
720
+ _formatReferenceLabel(ann, text) {
721
+ try {
722
+ const t = String(text ?? '');
723
+ if (!t) return t;
724
+ if (ann && (ann.isReference === true)) return `(${t})`;
725
+ return t;
726
+ } catch { return text; }
727
+ }
728
+
729
+ _composeLabelSVG(baseImage, labels, width, height, cssWidth = null, cssHeight = null) {
730
+ if (!baseImage) throw new Error('Base image missing for SVG composition');
731
+ const camera = this.viewer?.camera;
732
+ const safeCssWidth = Math.max(1, cssWidth || width);
733
+ const dpr = Math.max(1, width / safeCssWidth);
734
+ const paddingX = 8 * dpr;
735
+ const paddingY = 6 * dpr;
736
+ const lineHeight = 18 * dpr;
737
+ const radius = 8 * dpr;
738
+ const fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
739
+ const fontSize = 14 * dpr;
740
+
741
+ const layout = [];
742
+ labels.forEach((label) => {
743
+ if (!label || !label.world || label.text == null) return;
744
+ const screen = this._projectWorldToScreen(label.world, camera, { width, height });
745
+ if (!screen) return;
746
+ const lines = String(label.text).split(/\r?\n/);
747
+ const textWidth = lines.reduce((max, line) => Math.max(max, this._measureTextApprox(line, fontSize, fontFamily)), 0);
748
+ const boxWidth = textWidth + paddingX * 2;
749
+ const boxHeight = lines.length * lineHeight + paddingY * 2;
750
+ const { ox, oy } = this._resolveLabelAnchorOffsets(label.anchor);
751
+ const x = screen.x - ox * boxWidth;
752
+ const y = screen.y - oy * boxHeight;
753
+ layout.push({ x, y, boxWidth, boxHeight, lines });
754
+ });
755
+
756
+ if (!layout.length && labels.length) {
757
+ throw new Error('No label positions resolved for SVG composition');
758
+ }
759
+
760
+ const escape = (s) => this._escapeXML(String(s));
761
+ const rects = layout.map(({ x, y, boxWidth, boxHeight }) =>
762
+ `<rect x="${x.toFixed(3)}" y="${y.toFixed(3)}" rx="${radius}" ry="${radius}" width="${boxWidth.toFixed(3)}" height="${boxHeight.toFixed(3)}" fill="rgba(17,24,39,0.92)" stroke="#111827" stroke-width="1"/>`).join('');
763
+
764
+ const texts = layout.map(({ x, y, lines }) => {
765
+ const parts = [];
766
+ const startY = y + paddingY + lineHeight / 2;
767
+ const textX = x + paddingX;
768
+ lines.forEach((line, idx) => {
769
+ const ty = startY + lineHeight * idx;
770
+ parts.push(`<text x="${textX.toFixed(3)}" y="${ty.toFixed(3)}" font-family="${escape(fontFamily)}" font-size="${fontSize}" font-weight="700" fill="#ffffff" dominant-baseline="middle">${escape(line)}</text>`);
771
+ });
772
+ return parts.join('');
773
+ }).join('');
774
+
775
+ const svg = `
776
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
777
+ <image href="${baseImage}" x="0" y="0" width="${width}" height="${height}" />
778
+ ${rects}
779
+ ${texts}
780
+ </svg>
781
+ `;
782
+ return svg;
783
+ }
784
+
785
+ _measureTextApprox(text, fontSize = 14, _family = '') {
786
+ if (!text) return 0;
787
+ const avg = fontSize * 0.56; // rough average width per char
788
+ return Math.max(fontSize, avg * String(text).length);
789
+ }
790
+
791
+ async _svgToPngDataUrl(svgMarkup, width, height) {
792
+ const blob = new Blob([svgMarkup], { type: 'image/svg+xml;charset=utf-8' });
793
+ const url = URL.createObjectURL(blob);
794
+ try {
795
+ const img = new Image();
796
+ const dataUrl = await new Promise((resolve, reject) => {
797
+ img.onload = () => {
798
+ try {
799
+ const out = document.createElement('canvas');
800
+ out.width = width;
801
+ out.height = height;
802
+ const ctx = out.getContext('2d');
803
+ if (!ctx) { reject(new Error('No 2D context for SVG rasterization')); return; }
804
+ ctx.drawImage(img, 0, 0, width, height);
805
+ resolve(out.toDataURL('image/png'));
806
+ } catch (e) { reject(e); }
807
+ };
808
+ img.onerror = (e) => reject(e || new Error('Image load error for SVG'));
809
+ img.src = url;
810
+ });
811
+ return dataUrl;
812
+ } finally {
813
+ try { URL.revokeObjectURL(url); } catch { }
814
+ }
815
+ }
816
+
817
+ _escapeXML(str) {
818
+ return String(str)
819
+ .replace(/&/g, '&amp;')
820
+ .replace(/</g, '&lt;')
821
+ .replace(/>/g, '&gt;')
822
+ .replace(/"/g, '&quot;')
823
+ .replace(/'/g, '&#39;');
824
+ }
825
+
826
+ _dataUrlToUint8Array(dataUrl) {
827
+ if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:')) {
828
+ throw new Error('Invalid data URL for PNG export');
829
+ }
830
+ const parts = dataUrl.split(',');
831
+ if (parts.length < 2) throw new Error('Malformed data URL');
832
+ const base64 = parts[1];
833
+ const binary = atob(base64);
834
+ const len = binary.length;
835
+ const bytes = new Uint8Array(len);
836
+ for (let i = 0; i < len; i++) {
837
+ bytes[i] = binary.charCodeAt(i) & 0xff;
838
+ }
839
+ return bytes;
840
+ }
841
+
842
+ _safeFileName(raw, fallback = 'view') {
843
+ const s = String(raw || '').trim() || fallback;
844
+ return s.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 120) || fallback;
845
+ }
846
+
847
+
848
+ _injectExportStyles(doc) {
849
+ try {
850
+ if (!doc || doc.getElementById('pmi-export-styles')) return;
851
+ const style = doc.createElement('style');
852
+ style.id = 'pmi-export-styles';
853
+ style.textContent = `
854
+ body { margin: 16px; background: #0b0e14; color: #e5e7eb; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
855
+ .pmi-export-title { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
856
+ .pmi-export-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
857
+ .pmi-export-card { background: #0f172a; border: 1px solid #1f2937; border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 8px 20px rgba(0,0,0,.45); }
858
+ .pmi-export-card img { width: 100%; border-radius: 8px; background: #000; }
859
+ .pmi-export-caption { font-weight: 600; word-break: break-word; }
860
+ `;
861
+ doc.head.appendChild(style);
862
+ } catch { /* ignore style injection failures */ }
863
+ }
864
+
865
+ _awaitNextFrame() {
866
+ return new Promise((resolve) => requestAnimationFrame(resolve));
867
+ }
868
+
869
+ async _renderAndWait(frames = 1) {
870
+ const count = Math.max(1, frames | 0);
871
+ for (let i = 0; i < count; i++) {
872
+ try { this.viewer?.render?.(); } catch { }
873
+ await this._awaitNextFrame();
874
+ }
875
+ try { this.viewer?.render?.(); } catch { }
876
+ }
877
+
878
+ _withViewCubeHidden(fn) {
879
+ const cube = this.viewer?.viewCube || null;
880
+ if (!cube) return fn();
881
+ const prevRender = cube.render;
882
+ const prevVisible = cube.scene?.visible;
883
+ return (async () => {
884
+ try {
885
+ if (cube.scene) cube.scene.visible = false;
886
+ cube.render = () => {};
887
+ return await fn();
888
+ } finally {
889
+ cube.render = prevRender;
890
+ if (cube.scene && prevVisible !== undefined) cube.scene.visible = prevVisible;
891
+ }
892
+ })();
893
+ }
894
+
895
+ _restoreViewState(snapshot, wireframe) {
896
+ try {
897
+ const viewer = this.viewer;
898
+ if (snapshot && viewer?.camera) {
899
+ const dom = viewer?.renderer?.domElement;
900
+ const rect = dom?.getBoundingClientRect?.();
901
+ const viewport = {
902
+ width: rect?.width || dom?.width || 1,
903
+ height: rect?.height || dom?.height || 1,
904
+ };
905
+ applyCameraSnapshot(viewer.camera, snapshot, { controls: viewer.controls, respectParent: true, syncControls: true, viewport });
906
+ adjustOrthographicFrustum(viewer.camera, snapshot?.projection || null, viewport);
907
+ }
908
+ if (typeof wireframe === 'boolean') {
909
+ this._applyWireframe(viewer?.scene, wireframe);
910
+ }
911
+ try { viewer?.render?.(); } catch { }
912
+ } catch { /* ignore restore errors */ }
913
+ }
914
+
915
+ _applyView(view, { index = null, suppressActive = false } = {}) {
916
+ try {
917
+ const v = this.viewer;
918
+ const cam = v?.camera;
919
+ if (!cam || !view || !view.camera) return;
920
+
921
+ const ctrls = this.viewer?.controls;
922
+ const dom = this.viewer?.renderer?.domElement;
923
+ const rect = dom?.getBoundingClientRect?.();
924
+ const viewport = {
925
+ width: rect?.width || dom?.width || 1,
926
+ height: rect?.height || dom?.height || 1,
927
+ };
928
+ const applied = applyCameraSnapshot(cam, view.camera, { controls: ctrls, respectParent: true, syncControls: false, viewport });
929
+
930
+ if (!applied) {
931
+ // Fallback for legacy snapshots that somehow failed the structured restore
932
+ const legacy = view.camera;
933
+ if (legacy.position) {
934
+ cam.position.set(legacy.position.x, legacy.position.y, legacy.position.z);
935
+ }
936
+ if (legacy.quaternion) {
937
+ cam.quaternion.set(legacy.quaternion.x, legacy.quaternion.y, legacy.quaternion.z, legacy.quaternion.w);
938
+ }
939
+ if (legacy.up) {
940
+ cam.up.set(legacy.up.x, legacy.up.y, legacy.up.z);
941
+ }
942
+ if (typeof legacy.zoom === 'number' && Number.isFinite(legacy.zoom) && legacy.zoom > 0) {
943
+ cam.zoom = legacy.zoom;
944
+ }
945
+ if (legacy.target && ctrls) {
946
+ try {
947
+ if (typeof ctrls.setTarget === 'function') {
948
+ ctrls.setTarget(legacy.target.x, legacy.target.y, legacy.target.z);
949
+ } else if (ctrls.target) {
950
+ ctrls.target.set(legacy.target.x, legacy.target.y, legacy.target.z);
951
+ }
952
+ } catch { /* ignore */ }
953
+ }
954
+ adjustOrthographicFrustum(cam, legacy?.projection || null, viewport);
955
+ cam.updateMatrixWorld(true);
956
+ try { ctrls?.update?.(); } catch {}
957
+ }
958
+ adjustOrthographicFrustum(cam, view.camera?.projection || null, viewport);
959
+ try { ctrls?.updateMatrixState?.(); } catch {}
960
+ // Apply persisted view settings (e.g., wireframe) if present
961
+ try {
962
+ const vs = view.viewSettings || {};
963
+ if (typeof vs.wireframe === 'boolean') {
964
+ this._applyWireframe(v?.scene, vs.wireframe);
965
+ }
966
+ } catch { }
967
+ try { this.viewer.render(); } catch { }
968
+ if (!suppressActive && Number.isInteger(index)) {
969
+ this._setActiveViewIndex(index);
970
+ this._renderList();
971
+ }
972
+ } catch { /* ignore */ }
973
+ }
974
+
975
+ _setViewWireframe(index, isWireframe) {
976
+ const applyFlag = (entry) => {
977
+ if (!entry || typeof entry !== 'object') return entry;
978
+ if (!entry.viewSettings || typeof entry.viewSettings !== 'object') {
979
+ entry.viewSettings = {};
980
+ }
981
+ entry.viewSettings.wireframe = isWireframe;
982
+ return entry;
983
+ };
984
+
985
+ let updated = false;
986
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
987
+ if (manager && typeof manager.updateView === 'function') {
988
+ const result = manager.updateView(index, (entry) => applyFlag(entry));
989
+ updated = Boolean(result);
990
+ } else if (Array.isArray(this.views) && this.views[index]) {
991
+ applyFlag(this.views[index]);
992
+ updated = true;
993
+ this.refreshFromHistory();
994
+ }
995
+
996
+ if (!updated) {
997
+ this.refreshFromHistory();
998
+ this._renderList();
999
+ }
1000
+
1001
+ const activePMI = this.viewer?._pmiMode;
1002
+ if (activePMI && Number.isInteger(activePMI.viewIndex) && activePMI.viewIndex === index) {
1003
+ try {
1004
+ this._applyWireframe(this.viewer?.scene, isWireframe);
1005
+ } catch { /* ignore */ }
1006
+ }
1007
+ }
1008
+
1009
+ _updateViewCamera(index) {
1010
+ try {
1011
+ const camera = this.viewer?.camera;
1012
+ if (!camera) return;
1013
+ const ctrls = this.viewer?.controls;
1014
+ const snap = captureCameraSnapshot(camera, { controls: ctrls });
1015
+ if (!snap) return;
1016
+
1017
+ let updated = false;
1018
+ const manager = this.viewer?.partHistory?.pmiViewsManager;
1019
+ if (manager && typeof manager.updateView === 'function') {
1020
+ const result = manager.updateView(index, (entry) => {
1021
+ if (!entry || typeof entry !== 'object') return entry;
1022
+ entry.camera = snap;
1023
+ return entry;
1024
+ });
1025
+ updated = Boolean(result);
1026
+ } else if (Array.isArray(this.views) && this.views[index]) {
1027
+ this.views[index].camera = snap;
1028
+ updated = true;
1029
+ this.refreshFromHistory();
1030
+ }
1031
+
1032
+ if (!updated) {
1033
+ this.refreshFromHistory();
1034
+ this._renderList();
1035
+ }
1036
+ } catch { /* ignore */ }
1037
+ }
1038
+
1039
+ async _enterEditMode(view, index) {
1040
+ try {
1041
+ const activePMI = this.viewer?._pmiMode;
1042
+ if (activePMI) {
1043
+ try {
1044
+ await activePMI.finish();
1045
+ } catch (err) {
1046
+ console.warn('PMI Views: failed to finish active PMI session before switching', err);
1047
+ }
1048
+ }
1049
+ } catch (err) {
1050
+ console.warn('PMI Views: unexpected PMI session check failure', err);
1051
+ }
1052
+
1053
+ try { this._applyView(view, { index }); } catch {}
1054
+ try { this.viewer.startPMIMode?.(view, index, this); } catch {}
1055
+ }
1056
+
1057
+ // --- Helpers: view settings ---
1058
+ _isFaceObject(obj) {
1059
+ return !!obj && (obj.type === 'FACE' || (obj.isMesh && typeof obj.userData?.faceName === 'string'));
1060
+ }
1061
+
1062
+ _detectWireframe(scene) {
1063
+ try {
1064
+ if (!scene) return false;
1065
+ let wf = false;
1066
+ scene.traverse((obj) => {
1067
+ if (wf) return;
1068
+ if (!this._isFaceObject(obj)) return;
1069
+ const m = obj?.material;
1070
+ if (!m) return;
1071
+ if (Array.isArray(m)) {
1072
+ for (const mm of m) { if (mm && 'wireframe' in mm && mm.wireframe) { wf = true; break; } }
1073
+ } else if ('wireframe' in m && m.wireframe) {
1074
+ wf = true;
1075
+ }
1076
+ });
1077
+ return wf;
1078
+ } catch { return false; }
1079
+ }
1080
+
1081
+ _applyWireframe(scene, isWireframe) {
1082
+ try {
1083
+ if (!scene) return;
1084
+ const apply = (mat) => { if (mat && 'wireframe' in mat) mat.wireframe = !!isWireframe; };
1085
+ scene.traverse((obj) => {
1086
+ if (!this._isFaceObject(obj)) return;
1087
+ const m = obj?.material;
1088
+ if (!m) return;
1089
+ if (Array.isArray(m)) {
1090
+ for (const mm of m) apply(mm);
1091
+ } else {
1092
+ apply(m);
1093
+ }
1094
+ });
1095
+ } catch { /* ignore */ }
1096
+ }
1097
+
1098
+ }