brep-io-kernel 1.0.0-ci.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +157 -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,609 @@
1
+ import JSZip from 'jszip';
2
+ import { generate3MF } from '../../exporters/threeMF.js';
3
+ import { generateSTEP } from '../../exporters/step.js';
4
+ import { buildSheetMetalFlatPatternSvgs } from '../../exporters/sheetMetalFlatPattern.js';
5
+ import { FloatingWindow } from '../FloatingWindow.js';
6
+
7
+ async function _captureThumbnail(viewer, size = 256) {
8
+ try {
9
+ const canvas = viewer?.renderer?.domElement;
10
+ if (!canvas) return null;
11
+ const srcW = canvas.width || canvas.clientWidth || 1;
12
+ const srcH = canvas.height || canvas.clientHeight || 1;
13
+ const dst = document.createElement('canvas');
14
+ dst.width = size; dst.height = size;
15
+ const ctx = dst.getContext('2d');
16
+ if (!ctx) return null;
17
+ try { ctx.clearRect(0, 0, size, size); } catch {}
18
+ const scale = Math.min(size / srcW, size / srcH);
19
+ const dw = Math.max(1, Math.floor(srcW * scale));
20
+ const dh = Math.max(1, Math.floor(srcH * scale));
21
+ const dx = Math.floor((size - dw) / 2);
22
+ const dy = Math.floor((size - dh) / 2);
23
+ try { ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; } catch {}
24
+ ctx.drawImage(canvas, 0, 0, srcW, srcH, dx, dy, dw, dh);
25
+ return dst.toDataURL('image/png');
26
+ } catch { return null; }
27
+ }
28
+
29
+ export function createExportButton(viewer) {
30
+ const onClick = () => _openExportDialog(viewer);
31
+ return { label: '📤', title: 'Export…', onClick };
32
+ }
33
+
34
+ // Helpers moved from MainToolbar
35
+ function _ensureExportDialogStyles() {
36
+ if (document.getElementById('export-dialog-styles')) return;
37
+ const style = document.createElement('style');
38
+ style.id = 'export-dialog-styles';
39
+ style.textContent = `
40
+ .exp-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 11; }
41
+ .exp-modal { background: #0b0e14; color: #e5e7eb; border: 1px solid #1f2937; border-radius: 10px; padding: 14px; width: min(480px, calc(100vw - 32px)); box-shadow: 0 10px 40px rgba(0,0,0,.5); }
42
+ .exp-title { margin: 0 0 8px 0; font-size: 14px; font-weight: 700; }
43
+ .exp-row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
44
+ .exp-col { display: flex; flex-direction: column; gap: 6px; }
45
+ .exp-label { width: 90px; color: #9aa0aa; font-size: 12px; }
46
+ .exp-input, .exp-select { flex: 1 1 auto; padding: 6px 8px; border-radius: 8px; background: #0b0e14; color: #e5e7eb; border: 1px solid #374151; outline: none; font-size: 12px; }
47
+ .exp-input:focus, .exp-select:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.15); }
48
+ .exp-hint { color: #9aa0aa; font-size: 12px; margin-top: 6px; }
49
+ .exp-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
50
+ .exp-btn { background: rgba(255,255,255,.03); color: #f9fafb; border: 1px solid #374151; padding: 6px 10px; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 12px; line-height: 1; }
51
+ .exp-btn:hover { border-color: #3b82f6; background: rgba(59,130,246,.12); }
52
+ .exp-btn:active { transform: translateY(1px); }
53
+ `;
54
+ document.head.appendChild(style);
55
+ }
56
+
57
+ function _unitScale(unit) {
58
+ switch (String(unit || 'millimeter')) {
59
+ case 'millimeter': return 1;
60
+ case 'centimeter': return 0.1; // mm -> cm
61
+ case 'meter': return 0.001; // mm -> m
62
+ case 'micron': return 1000; // mm -> µm
63
+ case 'inch': return 1 / 25.4; // mm -> in
64
+ case 'foot': return 1 / 304.8; // mm -> ft
65
+ default: return 1;
66
+ }
67
+ }
68
+
69
+ function _meshToAsciiSTL(mesh, name = 'solid', precision = 6, scale = 1) {
70
+ const vp = mesh.vertProperties;
71
+ const tv = mesh.triVerts;
72
+ const fmt = (n) => Number.isFinite(n) ? n.toFixed(precision) : '0';
73
+ const out = [];
74
+ out.push(`solid ${name}`);
75
+ const triCount = (tv.length / 3) | 0;
76
+ for (let t = 0; t < triCount; t++) {
77
+ const i0 = tv[t * 3 + 0] >>> 0;
78
+ const i1 = tv[t * 3 + 1] >>> 0;
79
+ const i2 = tv[t * 3 + 2] >>> 0;
80
+ const ax = vp[i0 * 3 + 0] * scale, ay = vp[i0 * 3 + 1] * scale, az = vp[i0 * 3 + 2] * scale;
81
+ const bx = vp[i1 * 3 + 0] * scale, by = vp[i1 * 3 + 1] * scale, bz = vp[i1 * 3 + 2] * scale;
82
+ const cx = vp[i2 * 3 + 0] * scale, cy = vp[i2 * 3 + 1] * scale, cz = vp[i2 * 3 + 2] * scale;
83
+ const ux = bx - ax, uy = by - ay, uz = bz - az;
84
+ const vx = cx - ax, vy = cy - ay, vz = cz - az;
85
+ let nx = uy * vz - uz * vy;
86
+ let ny = uz * vx - ux * vz;
87
+ let nz = ux * vy - uy * vx;
88
+ const nl = Math.hypot(nx, ny, nz) || 1;
89
+ nx /= nl; ny /= nl; nz /= nl;
90
+ out.push(` facet normal ${fmt(nx)} ${fmt(ny)} ${fmt(nz)}`);
91
+ out.push(' outer loop');
92
+ out.push(` vertex ${fmt(ax)} ${fmt(ay)} ${fmt(az)}`);
93
+ out.push(` vertex ${fmt(bx)} ${fmt(by)} ${fmt(bz)}`);
94
+ out.push(` vertex ${fmt(cx)} ${fmt(cy)} ${fmt(cz)}`);
95
+ out.push(' endloop');
96
+ out.push(' endfacet');
97
+ }
98
+ out.push(`endsolid ${name}`);
99
+ return out.join('\n');
100
+ }
101
+
102
+ function _meshToAsciiOBJ(mesh, name = 'object', precision = 6, scale = 1) {
103
+ const vp = mesh.vertProperties;
104
+ const tv = mesh.triVerts;
105
+ const fmt = (n) => Number.isFinite(n) ? n.toFixed(precision) : '0';
106
+ const out = [];
107
+ // Object/group name (safe ASCII)
108
+ out.push(`# Exported by BREP`);
109
+ out.push(`o ${name}`);
110
+ // Emit unique vertices referenced by triVerts to keep file smaller
111
+ const indexMap = new Map(); // original index -> 1-based OBJ index
112
+ let nextIndex = 1;
113
+ const faces = []; // store triples of mapped indices
114
+ const triCount = (tv.length / 3) | 0;
115
+ for (let t = 0; t < triCount; t++) {
116
+ const i0 = tv[t * 3 + 0] >>> 0;
117
+ const i1 = tv[t * 3 + 1] >>> 0;
118
+ const i2 = tv[t * 3 + 2] >>> 0;
119
+ const mapIndex = (i) => {
120
+ let id = indexMap.get(i);
121
+ if (!id) {
122
+ const x = vp[i * 3 + 0] * scale;
123
+ const y = vp[i * 3 + 1] * scale;
124
+ const z = vp[i * 3 + 2] * scale;
125
+ out.push(`v ${fmt(x)} ${fmt(y)} ${fmt(z)}`);
126
+ id = nextIndex++;
127
+ indexMap.set(i, id);
128
+ }
129
+ return id;
130
+ };
131
+ const a = mapIndex(i0), b = mapIndex(i1), c = mapIndex(i2);
132
+ faces.push([a, b, c]);
133
+ }
134
+ // Faces (referencing v indices; no normals/UVs)
135
+ for (const f of faces) out.push(`f ${f[0]} ${f[1]} ${f[2]}`);
136
+ return out.join('\n');
137
+ }
138
+
139
+ function _collectSolids(viewer) {
140
+ const scene = viewer?.partHistory?.scene || viewer?.scene;
141
+ if (!scene) return [];
142
+ const solids = [];
143
+ scene.traverse((o) => {
144
+ if (!o || !o.visible) return;
145
+ if (o.type === 'SOLID' && typeof o.toSTL === 'function') solids.push(o);
146
+ });
147
+ const selected = solids.filter(o => o.selected === true);
148
+ return selected.length ? selected : solids;
149
+ }
150
+
151
+ function _download(filename, data, mime = 'application/octet-stream') {
152
+ const blob = new Blob([data], { type: mime });
153
+ const url = URL.createObjectURL(blob);
154
+ const a = document.createElement('a');
155
+ a.href = url;
156
+ a.download = filename;
157
+ document.body.appendChild(a);
158
+ a.click();
159
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
160
+ }
161
+
162
+ function _safeName(raw, fallback = 'solid') {
163
+ const s = String(raw || '').trim();
164
+ return (s.length ? s : fallback).replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 80);
165
+ }
166
+
167
+ function _ensureFlatPatternDebugStyles() {
168
+ if (document.getElementById('flat-pattern-debug-window-styles')) return;
169
+ const style = document.createElement('style');
170
+ style.id = 'flat-pattern-debug-window-styles';
171
+ style.textContent = `
172
+ .flat-debug-content { display:flex; flex-direction:column; gap:12px; padding:8px; width:100%; height:100%; box-sizing:border-box; overflow:auto; }
173
+ .flat-debug-title { font-size:13px; font-weight:700; color:#e5e7eb; }
174
+ .flat-debug-section { display:flex; flex-direction:column; gap:8px; }
175
+ .flat-debug-section-title { font-size:12px; color:#9aa0aa; text-transform:none; letter-spacing:.2px; }
176
+ .flat-debug-step { padding:10px; border:1px solid #1f2937; border-radius:8px; background:#111827; display:flex; flex-direction:column; gap:6px; }
177
+ .flat-debug-svg { background:#fff; border-radius:6px; padding:6px; display:block; max-width:100%; overflow:auto; height:300px; }
178
+ .flat-debug-svg svg { display:block; max-width:100%; width:100%; height:100%; }
179
+ .flat-debug-empty { font-size:12px; color:#9aa0aa; }
180
+ `;
181
+ document.head.appendChild(style);
182
+ }
183
+
184
+ function _ensureFlatPatternDebugPanel(viewer, title) {
185
+ if (!viewer) return null;
186
+ _ensureFlatPatternDebugStyles();
187
+ if (viewer.__flatPatternDebugPanel && viewer.__flatPatternDebugPanel.window) {
188
+ const panel = viewer.__flatPatternDebugPanel;
189
+ try { panel.window.setTitle(title || 'Flat Pattern Debug'); } catch {}
190
+ try { panel.root.style.display = 'flex'; } catch {}
191
+ try { panel.window.bringToFront(); } catch {}
192
+ return panel;
193
+ }
194
+ let panel = null;
195
+ const height = Math.max(260, Math.floor((window?.innerHeight || 800) * 0.7));
196
+ const fw = new FloatingWindow({
197
+ title: title || 'Flat Pattern Debug',
198
+ width: 760,
199
+ height,
200
+ right: 12,
201
+ top: 80,
202
+ shaded: false,
203
+ onClose: () => {
204
+ if (panel && panel.root) {
205
+ try { panel.root.style.display = 'none'; } catch {}
206
+ }
207
+ if (panel) panel.open = false;
208
+ },
209
+ });
210
+ const content = document.createElement('div');
211
+ content.className = 'flat-debug-content';
212
+ fw.content.appendChild(content);
213
+ panel = { window: fw, root: fw.root, content, open: true };
214
+ viewer.__flatPatternDebugPanel = panel;
215
+ return panel;
216
+ }
217
+
218
+ function _renderFlatPatternDebugPanel(panel, entries, baseName) {
219
+ if (!panel || !panel.content) return;
220
+ panel.content.innerHTML = '';
221
+ const title = document.createElement('div');
222
+ title.className = 'flat-debug-title';
223
+ title.textContent = baseName ? `${baseName} Flat Pattern Debug` : 'Flat Pattern Debug';
224
+ panel.content.appendChild(title);
225
+
226
+ let hasPreview = false;
227
+ if (Array.isArray(entries)) {
228
+ for (const entry of entries) {
229
+ if (!entry || !entry.svg) continue;
230
+ hasPreview = true;
231
+ const section = document.createElement('div');
232
+ section.className = 'flat-debug-section';
233
+ const sectionTitle = document.createElement('div');
234
+ sectionTitle.className = 'flat-debug-section-title';
235
+ sectionTitle.textContent = entry.name || 'Flat Pattern';
236
+ section.appendChild(sectionTitle);
237
+ const stepWrap = document.createElement('div');
238
+ stepWrap.className = 'flat-debug-step';
239
+ const svgWrap = document.createElement('div');
240
+ svgWrap.className = 'flat-debug-svg';
241
+ const cleaned = String(entry.svg || '').replace(/^<\\?xml[^>]*>\\s*/i, '');
242
+ svgWrap.innerHTML = cleaned;
243
+ stepWrap.appendChild(svgWrap);
244
+ section.appendChild(stepWrap);
245
+ panel.content.appendChild(section);
246
+ }
247
+ }
248
+ if (!hasPreview) {
249
+ const empty = document.createElement('div');
250
+ empty.className = 'flat-debug-empty';
251
+ empty.textContent = 'No flat pattern previews available.';
252
+ panel.content.appendChild(empty);
253
+ }
254
+ }
255
+
256
+ function _openExportDialog(viewer) {
257
+ _ensureExportDialogStyles();
258
+ const solids = _collectSolids(viewer);
259
+ if (!solids.length) { alert('No solids to export.'); return; }
260
+
261
+ const overlay = document.createElement('div');
262
+ overlay.className = 'exp-modal-overlay';
263
+ const modal = document.createElement('div');
264
+ modal.className = 'exp-modal';
265
+
266
+ const title = document.createElement('div');
267
+ title.className = 'exp-title';
268
+ title.textContent = 'Export';
269
+
270
+ const baseDefault = _safeName(viewer?.fileManagerWidget?.currentName || solids[0]?.name || 'part');
271
+
272
+ // Filename
273
+ const rowName = document.createElement('div'); rowName.className = 'exp-row';
274
+ const labName = document.createElement('div'); labName.className = 'exp-label'; labName.textContent = 'Filename';
275
+ const inpName = document.createElement('input'); inpName.className = 'exp-input'; inpName.value = baseDefault;
276
+ rowName.appendChild(labName); rowName.appendChild(inpName);
277
+
278
+ // Format
279
+ const rowFmt = document.createElement('div'); rowFmt.className = 'exp-row';
280
+ const labFmt = document.createElement('div'); labFmt.className = 'exp-label'; labFmt.textContent = 'Format';
281
+ const selFmt = document.createElement('select'); selFmt.className = 'exp-select';
282
+ const opt3mf = document.createElement('option'); opt3mf.value = '3mf'; opt3mf.textContent = '3MF (+history)'; selFmt.appendChild(opt3mf);
283
+ const optStl = document.createElement('option'); optStl.value = 'stl'; optStl.textContent = 'STL (ASCII)'; selFmt.appendChild(optStl);
284
+ const optStep = document.createElement('option'); optStep.value = 'step'; optStep.textContent = 'STEP (faceted)'; selFmt.appendChild(optStep);
285
+ const optJson = document.createElement('option'); optJson.value = 'json'; optJson.textContent = 'BREP JSON (history only)'; selFmt.appendChild(optJson);
286
+ const optObj = document.createElement('option'); optObj.value = 'obj'; optObj.textContent = 'OBJ (ASCII)'; selFmt.appendChild(optObj);
287
+ rowFmt.appendChild(labFmt); rowFmt.appendChild(selFmt);
288
+
289
+ // Units
290
+ const rowUnit = document.createElement('div'); rowUnit.className = 'exp-row';
291
+ const labUnit = document.createElement('div'); labUnit.className = 'exp-label'; labUnit.textContent = 'Units';
292
+ const selUnit = document.createElement('select'); selUnit.className = 'exp-select';
293
+ const units = [
294
+ ['millimeter', 'Millimeters (mm)'],
295
+ ['centimeter', 'Centimeters (cm)'],
296
+ ['meter', 'Meters (m)'],
297
+ ['micron', 'Microns (µm)'],
298
+ ['inch', 'Inches (in)'],
299
+ ['foot', 'Feet (ft)'],
300
+ ];
301
+ for (const [v, label] of units) { const o = document.createElement('option'); o.value = v; o.textContent = label; selUnit.appendChild(o); }
302
+ try { selUnit.value = 'millimeter'; } catch {}
303
+ rowUnit.appendChild(labUnit); rowUnit.appendChild(selUnit);
304
+
305
+ // STEP tessellation options
306
+ const rowTess = document.createElement('div'); rowTess.className = 'exp-row';
307
+ const labTess = document.createElement('div'); labTess.className = 'exp-label'; labTess.textContent = 'STEP';
308
+ const chkTess = document.createElement('input'); chkTess.type = 'checkbox'; chkTess.checked = false;
309
+ const tessWrap = document.createElement('label');
310
+ tessWrap.style.display = 'flex';
311
+ tessWrap.style.alignItems = 'center';
312
+ tessWrap.style.gap = '6px';
313
+ tessWrap.appendChild(chkTess);
314
+ tessWrap.appendChild(document.createTextNode('Use tessellated faces (AP242)'));
315
+ rowTess.appendChild(labTess); rowTess.appendChild(tessWrap);
316
+
317
+ const rowStepFaces = document.createElement('div'); rowStepFaces.className = 'exp-row';
318
+ const labStepFaces = document.createElement('div'); labStepFaces.className = 'exp-label'; labStepFaces.textContent = 'STEP';
319
+ const chkStepFaces = document.createElement('input'); chkStepFaces.type = 'checkbox'; chkStepFaces.checked = true;
320
+ const stepFacesWrap = document.createElement('label');
321
+ stepFacesWrap.style.display = 'flex';
322
+ stepFacesWrap.style.alignItems = 'center';
323
+ stepFacesWrap.style.gap = '6px';
324
+ stepFacesWrap.appendChild(chkStepFaces);
325
+ stepFacesWrap.appendChild(document.createTextNode('Export faces'));
326
+ rowStepFaces.appendChild(labStepFaces); rowStepFaces.appendChild(stepFacesWrap);
327
+
328
+ const rowStepEdges = document.createElement('div'); rowStepEdges.className = 'exp-row';
329
+ const labStepEdges = document.createElement('div'); labStepEdges.className = 'exp-label'; labStepEdges.textContent = 'STEP';
330
+ const chkStepEdges = document.createElement('input'); chkStepEdges.type = 'checkbox'; chkStepEdges.checked = true;
331
+ const stepEdgesWrap = document.createElement('label');
332
+ stepEdgesWrap.style.display = 'flex';
333
+ stepEdgesWrap.style.alignItems = 'center';
334
+ stepEdgesWrap.style.gap = '6px';
335
+ stepEdgesWrap.appendChild(chkStepEdges);
336
+ stepEdgesWrap.appendChild(document.createTextNode('Export edges as polylines'));
337
+ rowStepEdges.appendChild(labStepEdges); rowStepEdges.appendChild(stepEdgesWrap);
338
+
339
+ // Flat pattern options (3MF only)
340
+ const rowFlat = document.createElement('div'); rowFlat.className = 'exp-row';
341
+ const labFlat = document.createElement('div'); labFlat.className = 'exp-label'; labFlat.textContent = 'Flat';
342
+ const chkFlat = document.createElement('input'); chkFlat.type = 'checkbox'; chkFlat.checked = true;
343
+ const flatWrap = document.createElement('label');
344
+ flatWrap.style.display = 'flex';
345
+ flatWrap.style.alignItems = 'center';
346
+ flatWrap.style.gap = '6px';
347
+ flatWrap.appendChild(chkFlat);
348
+ flatWrap.appendChild(document.createTextNode('Include flat pattern'));
349
+ rowFlat.appendChild(labFlat); rowFlat.appendChild(flatWrap);
350
+
351
+ const rowNeutral = document.createElement('div'); rowNeutral.className = 'exp-row';
352
+ const labNeutral = document.createElement('div'); labNeutral.className = 'exp-label'; labNeutral.textContent = 'Neutral';
353
+ const inpNeutral = document.createElement('input'); inpNeutral.className = 'exp-input';
354
+ inpNeutral.type = 'number'; inpNeutral.min = '0'; inpNeutral.max = '1'; inpNeutral.step = '0.01';
355
+ inpNeutral.placeholder = 'auto';
356
+ inpNeutral.value = '';
357
+ rowNeutral.appendChild(labNeutral); rowNeutral.appendChild(inpNeutral);
358
+
359
+ const rowDebug = document.createElement('div'); rowDebug.className = 'exp-row';
360
+ const labDebug = document.createElement('div'); labDebug.className = 'exp-label'; labDebug.textContent = 'Debug';
361
+ const chkDebug = document.createElement('input'); chkDebug.type = 'checkbox'; chkDebug.checked = false;
362
+ const debugWrap = document.createElement('label');
363
+ debugWrap.style.display = 'flex';
364
+ debugWrap.style.alignItems = 'center';
365
+ debugWrap.style.gap = '6px';
366
+ debugWrap.appendChild(chkDebug);
367
+ debugWrap.appendChild(document.createTextNode('Show flat pattern preview'));
368
+ rowDebug.appendChild(labDebug); rowDebug.appendChild(debugWrap);
369
+
370
+ // Toggle unit row visibility based on format
371
+ const updateUnitVisibility = () => {
372
+ const fmt = selFmt.value;
373
+ rowUnit.style.display = (fmt === 'stl' || fmt === '3mf' || fmt === 'obj' || fmt === 'step') ? 'flex' : 'none';
374
+ rowTess.style.display = (fmt === 'step') ? 'flex' : 'none';
375
+ rowStepFaces.style.display = (fmt === 'step') ? 'flex' : 'none';
376
+ rowStepEdges.style.display = (fmt === 'step') ? 'flex' : 'none';
377
+ rowFlat.style.display = (fmt === '3mf') ? 'flex' : 'none';
378
+ rowNeutral.style.display = (fmt === '3mf' && chkFlat.checked) ? 'flex' : 'none';
379
+ rowDebug.style.display = (fmt === '3mf' && chkFlat.checked) ? 'flex' : 'none';
380
+ };
381
+ selFmt.addEventListener('change', updateUnitVisibility);
382
+ chkFlat.addEventListener('change', updateUnitVisibility);
383
+ updateUnitVisibility();
384
+
385
+ const hint = document.createElement('div'); hint.className = 'exp-hint'; hint.textContent = '3MF includes feature history when available. STL/OBJ/STEP export triangulated meshes. BREP JSON saves editable feature history only.';
386
+
387
+ // Buttons
388
+ const buttons = document.createElement('div'); buttons.className = 'exp-buttons';
389
+ const btnCancel = document.createElement('button'); btnCancel.className = 'exp-btn'; btnCancel.textContent = 'Cancel';
390
+ btnCancel.addEventListener('click', () => { try { document.body.removeChild(overlay); } catch {} });
391
+ const btnExport = document.createElement('button'); btnExport.className = 'exp-btn'; btnExport.textContent = 'Export';
392
+
393
+ const close = () => { try { document.body.removeChild(overlay); } catch {} };
394
+
395
+ btnExport.addEventListener('click', async () => {
396
+ try {
397
+ const base = String(inpName.value || baseDefault).trim() || baseDefault;
398
+ const fmt = selFmt.value;
399
+ const unit = selUnit.value;
400
+ const scale = _unitScale(unit);
401
+ const showDebug = fmt === '3mf' && chkFlat.checked && chkDebug.checked;
402
+ const debugPanel = showDebug ? _ensureFlatPatternDebugPanel(viewer, `${base} Flat Pattern Debug`) : null;
403
+
404
+ if (fmt === 'json') {
405
+ try {
406
+ const json = await viewer?.partHistory?.toJSON?.();
407
+ const text = typeof json === 'string' ? json : JSON.stringify(json || {});
408
+ _download(`${base}.BREP.json`, text, 'application/json');
409
+ close();
410
+ return;
411
+ } catch (e) { /* fall through to show alert below */ }
412
+ }
413
+
414
+ if (fmt === '3mf') {
415
+ // Possibly include feature history in metadata
416
+ let additionalFiles = undefined;
417
+ let modelMetadata = undefined;
418
+ try {
419
+ const json = await viewer?.partHistory?.toJSON?.();
420
+ if (json && typeof json === 'string') {
421
+ additionalFiles = { 'Metadata/featureHistory.json': json };
422
+ modelMetadata = { featureHistoryPath: '/Metadata/featureHistory.json' };
423
+ }
424
+ const viewFiles = await viewer?.pmiViewsWidget?.captureViewImagesForPackage?.();
425
+ if (viewFiles && typeof viewFiles === 'object') {
426
+ additionalFiles = { ...(additionalFiles || {}), ...viewFiles };
427
+ }
428
+ } catch {}
429
+
430
+ // Gracefully handle non-manifold solids by skipping them
431
+ const solidsForExport = [];
432
+ const skipped = [];
433
+ solids.forEach((s, idx) => {
434
+ try {
435
+ const mesh = s?.getMesh?.();
436
+ if (mesh && mesh.vertProperties && mesh.triVerts) {
437
+ solidsForExport.push(s);
438
+ } else {
439
+ const name = _safeName(s?.name || `solid_${idx}`);
440
+ skipped.push(name);
441
+ }
442
+ } catch (e) {
443
+ const name = _safeName(s?.name || `solid_${idx}`);
444
+ skipped.push(name);
445
+ }
446
+ });
447
+
448
+ // Capture a preview thumbnail to embed (best-effort)
449
+ const thumbnail = await _captureThumbnail(viewer, 256);
450
+
451
+ let data;
452
+ try {
453
+ const metadataManager = viewer?.partHistory?.metadataManager || null;
454
+ const includeFlat = chkFlat.checked;
455
+ const neutralRaw = String(inpNeutral.value || '').trim();
456
+ const neutralFactor = neutralRaw ? Number(neutralRaw) : null;
457
+ if (includeFlat) {
458
+ const svgEntries = buildSheetMetalFlatPatternSvgs(solidsForExport, {
459
+ neutralFactor,
460
+ metadataManager,
461
+ debug: !!debugPanel,
462
+ });
463
+ if (svgEntries.length) {
464
+ const svgPaths = [];
465
+ const svgFiles = {};
466
+ for (const entry of svgEntries) {
467
+ const safe = _safeName(entry.name || 'flat');
468
+ const path = `Metadata/flatpattern_${safe}.svg`;
469
+ svgFiles[path] = entry.svg;
470
+ svgPaths.push(`/${path}`);
471
+ }
472
+ additionalFiles = { ...(additionalFiles || {}), ...svgFiles };
473
+ if (!modelMetadata) modelMetadata = {};
474
+ modelMetadata.sheetMetalFlatPatternPaths = JSON.stringify(svgPaths);
475
+ }
476
+ if (debugPanel) {
477
+ _renderFlatPatternDebugPanel(debugPanel, svgEntries, base);
478
+ }
479
+ }
480
+ data = await generate3MF(solidsForExport, { unit, precision: 6, scale, additionalFiles, modelMetadata, thumbnail, metadataManager });
481
+ } catch (e) {
482
+ // As a last resort, attempt exporting only the feature history (no solids)
483
+ try {
484
+ const metadataManager = viewer?.partHistory?.metadataManager || null;
485
+ data = await generate3MF([], { unit, precision: 6, scale, additionalFiles, modelMetadata, thumbnail, metadataManager });
486
+ } catch (e2) {
487
+ throw e; // fall back to outer error handler
488
+ }
489
+ }
490
+
491
+ _download(`${base}.3mf`, data, 'model/3mf');
492
+ close();
493
+ if (skipped.length > 0) {
494
+ const msg = (solidsForExport.length === 0)
495
+ ? `Exported 3MF with feature history only. Skipped non-manifold solids: ${skipped.join(', ')}`
496
+ : `Exported 3MF. Skipped non-manifold solids: ${skipped.join(', ')}`;
497
+ try { alert(msg); } catch {}
498
+ }
499
+ return;
500
+ }
501
+
502
+ if (fmt === 'step') {
503
+ if (!chkStepFaces.checked && !chkStepEdges.checked) {
504
+ try { alert('STEP export: enable faces and/or edges.'); } catch {}
505
+ return;
506
+ }
507
+ const { data, exported, skipped } = generateSTEP(solids, {
508
+ name: base,
509
+ unit,
510
+ precision: 6,
511
+ scale,
512
+ applyWorldTransform: true,
513
+ useTessellatedFaces: chkTess.checked,
514
+ exportFaces: chkStepFaces.checked,
515
+ exportEdgesAsPolylines: chkStepEdges.checked,
516
+ });
517
+ if (!exported) {
518
+ const msg = skipped.length
519
+ ? `STEP export failed. Skipped solids: ${skipped.join(', ')}`
520
+ : 'STEP export failed.';
521
+ try { alert(msg); } catch {}
522
+ return;
523
+ }
524
+ _download(`${base}.step`, data, 'application/step');
525
+ close();
526
+ if (skipped.length > 0) {
527
+ try { alert(`Exported STEP. Skipped solids: ${skipped.join(', ')}`); } catch {}
528
+ }
529
+ return;
530
+ }
531
+
532
+ if (fmt === 'obj') {
533
+ // Single solid -> OBJ
534
+ if (solids.length === 1) {
535
+ const s = solids[0];
536
+ const mesh = s.getMesh();
537
+ const obj = _meshToAsciiOBJ(mesh, base, 6, scale);
538
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch {}
539
+ _download(`${base}.obj`, obj, 'text/plain');
540
+ close();
541
+ return;
542
+ }
543
+ // Multiple solids -> ZIP of individual OBJs
544
+ const zip = new JSZip();
545
+ solids.forEach((s, idx) => {
546
+ try {
547
+ const safe = _safeName(s.name || `solid_${idx}`);
548
+ const mesh = s.getMesh();
549
+ const obj = _meshToAsciiOBJ(mesh, safe, 6, scale);
550
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch {}
551
+ zip.file(`${safe}.obj`, obj);
552
+ } catch {}
553
+ });
554
+ const blob = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } });
555
+ _download(`${base}_obj.zip`, blob, 'application/zip');
556
+ close();
557
+ return;
558
+ }
559
+
560
+ // STL path
561
+ if (solids.length === 1) {
562
+ const s = solids[0];
563
+ const mesh = s.getMesh();
564
+ const stl = _meshToAsciiSTL(mesh, base, 6, scale);
565
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch {}
566
+ _download(`${base}.stl`, stl, 'model/stl');
567
+ close();
568
+ return;
569
+ }
570
+ // Multiple solids -> ZIP of individual STLs
571
+ const zip = new JSZip();
572
+ solids.forEach((s, idx) => {
573
+ try {
574
+ const safe = _safeName(s.name || `solid_${idx}`);
575
+ const mesh = s.getMesh();
576
+ const stl = _meshToAsciiSTL(mesh, safe, 6, scale);
577
+ try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch {}
578
+ zip.file(`${safe}.stl`, stl);
579
+ } catch {}
580
+ });
581
+ const blob = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } });
582
+ _download(`${base}_stl.zip`, blob, 'application/zip');
583
+ close();
584
+ } catch (e) {
585
+ alert('Export failed. See console for details.');
586
+ console.error(e);
587
+ }
588
+ });
589
+
590
+ buttons.appendChild(btnCancel);
591
+ buttons.appendChild(btnExport);
592
+
593
+ modal.appendChild(title);
594
+ modal.appendChild(rowName);
595
+ modal.appendChild(rowFmt);
596
+ modal.appendChild(rowUnit);
597
+ modal.appendChild(rowTess);
598
+ modal.appendChild(rowStepFaces);
599
+ modal.appendChild(rowStepEdges);
600
+ modal.appendChild(rowFlat);
601
+ modal.appendChild(rowNeutral);
602
+ modal.appendChild(rowDebug);
603
+ modal.appendChild(hint);
604
+ modal.appendChild(buttons);
605
+ overlay.appendChild(modal);
606
+ document.body.appendChild(overlay);
607
+
608
+ try { inpName.focus(); inpName.select(); } catch {}
609
+ }