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,615 @@
1
+ // fileManagerWidget.js
2
+ // A lightweight widget to save/load/delete models using IndexedDB storage.
3
+ // Designed to be embedded as an Accordion section (similar to expressionsManager).
4
+ import * as THREE from 'three';
5
+ import JSZip from 'jszip';
6
+ import { generate3MF } from '../exporters/threeMF.js';
7
+ import { localStorage as LS } from '../idbStorage.js';
8
+ import {
9
+ listComponentRecords,
10
+ getComponentRecord,
11
+ setComponentRecord,
12
+ removeComponentRecord,
13
+ MODEL_STORAGE_PREFIX,
14
+ uint8ArrayToBase64,
15
+ base64ToUint8Array,
16
+ } from '../services/componentLibrary.js';
17
+ import { HISTORY_COLLECTION_REFRESH_EVENT } from './history/HistoryCollectionWidget.js';
18
+
19
+ export class FileManagerWidget {
20
+ constructor(viewer) {
21
+ this.viewer = viewer;
22
+ this.uiElement = document.createElement('div');
23
+ // Per-model storage prefix
24
+ this._modelPrefix = MODEL_STORAGE_PREFIX;
25
+ this._lastKey = '__BREP_MODELS_LASTNAME__';
26
+ this.currentName = this._loadLastName() || '';
27
+ this._iconsOnly = this._loadIconsPref();
28
+ this._loadSeq = 0; // guards async load races
29
+ this._thumbCache = new Map();
30
+ this._ensureStyles();
31
+ this._buildUI();
32
+ this.refreshList();
33
+
34
+ // Refresh UI thumbnails/list when any model key changes via storage events (cross-tab and other code paths)
35
+ try {
36
+ this._onStorage = (ev) => {
37
+ try {
38
+ const key = (ev && (ev.key ?? (ev.detail && ev.detail.key))) || '';
39
+ if (!key) return;
40
+ if (key.startsWith(this._modelPrefix)) {
41
+ // Invalidate cache for this model and refresh list
42
+ try {
43
+ const encName = key.slice(this._modelPrefix.length);
44
+ const name = decodeURIComponent(encName);
45
+ if (name) this._thumbCache.delete(name);
46
+ } catch { }
47
+ this.refreshList();
48
+ } else if (key === this._lastKey || key === '__BREP_FM_ICONSVIEW__') {
49
+ // Preferences updated elsewhere; re-sync
50
+ this.currentName = this._loadLastName() || this.currentName || '';
51
+ this._iconsOnly = this._loadIconsPref();
52
+ this.refreshList();
53
+ }
54
+ } catch { /* ignore */ }
55
+ };
56
+ window.addEventListener('storage', this._onStorage);
57
+ } catch { /* ignore */ }
58
+
59
+ // Ensure storage hydration completes, then re-sync prefs/list and auto-load last
60
+ try {
61
+ Promise.resolve(LS.ready()).then(() => {
62
+ try {
63
+ this.currentName = this._loadLastName() || this.currentName || '';
64
+ this._iconsOnly = this._loadIconsPref();
65
+ this.refreshList();
66
+ this.autoLoadLast();
67
+ } catch { alert('Failed to initialize File Manager storage.'); }
68
+ });
69
+ } catch { alert('Failed to initialize File Manager storage.'); }
70
+ }
71
+
72
+
73
+ async autoLoadLast() {
74
+ if (await confirm('Load the last opened model?', 5)) {
75
+ try {
76
+ const last = this._loadLastName();
77
+ if (last) {
78
+ const exists = this._getModel(last);
79
+ if (exists) {
80
+ // Fire and forget; constructor cannot be async
81
+ this.loadModel(last);
82
+ }
83
+ }
84
+ } catch { /* ignore auto-load failures */ }
85
+ }
86
+
87
+ }
88
+
89
+
90
+
91
+ // ----- Storage helpers -----
92
+ // List all saved model records from per-model keys
93
+ _listModels() {
94
+ const records = listComponentRecords();
95
+ return records.map(({ name, savedAt, record }) => ({
96
+ name,
97
+ savedAt,
98
+ data: record?.data,
99
+ data3mf: record?.data3mf,
100
+ thumbnail: record?.thumbnail,
101
+ }));
102
+ }
103
+ // Fetch one model record
104
+ _getModel(name) {
105
+ return getComponentRecord(name);
106
+ }
107
+ // Persist one model record
108
+ _setModel(name, dataObj) {
109
+ setComponentRecord(name, dataObj);
110
+ }
111
+ // Remove one model record
112
+ _removeModel(name) {
113
+ removeComponentRecord(name);
114
+ }
115
+ _saveLastName(name) {
116
+ if (name) LS.setItem(this._lastKey, name);
117
+ }
118
+ _loadLastName() {
119
+ return LS.getItem(this._lastKey) || '';
120
+ }
121
+ _saveIconsPref(v) {
122
+ try { LS.setItem('__BREP_FM_ICONSVIEW__', v ? '1' : '0'); } catch { }
123
+ }
124
+ _loadIconsPref() {
125
+ try { return LS.getItem('__BREP_FM_ICONSVIEW__') === '1'; } catch { return false; }
126
+ }
127
+
128
+
129
+
130
+ // ----- UI -----
131
+ _ensureStyles() {
132
+ if (document.getElementById('file-manager-widget-styles')) return;
133
+ const style = document.createElement('style');
134
+ style.id = 'file-manager-widget-styles';
135
+ style.textContent = `
136
+ /* Layout */
137
+ .fm-row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; border-bottom: 1px solid #1f2937; background: transparent; transition: background-color .12s ease; }
138
+ .fm-row:hover { background: #0f172a; }
139
+ .fm-row.header { background: #111827; border-bottom: 1px solid #1f2937; padding-bottom: 8px; margin-bottom: 4px; }
140
+ .fm-row:last-child { border-bottom: 0; }
141
+ .fm-grow { flex: 1 1 auto; overflow: hidden; }
142
+ .fm-thumb { flex: 0 0 auto; width: 60px; height: 60px; border-radius: 6px; border: 1px solid #1f2937; background: #0b0e14; object-fit: contain; image-rendering: auto; }
143
+
144
+ /* Inputs (keep text size and padding) */
145
+ .fm-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; }
146
+ .fm-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.15); }
147
+
148
+ /* Buttons (keep text size and padding) */
149
+ .fm-btn { background: rgba(255,255,255,.03); color: #f9fafb; border: 1px solid #374151; padding: 2px 6px; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 12px; line-height: 1; min-width: 26px; height: 24px; display: inline-flex; align-items: center; justify-content: center; transition: border-color .15s ease, background-color .15s ease, transform .05s ease; }
150
+ .fm-btn:hover { border-color: #3b82f6; background: rgba(59,130,246,.12); }
151
+ .fm-btn:active { transform: translateY(1px); }
152
+ .fm-btn.danger { border-color: #7f1d1d; color: #fecaca; }
153
+ .fm-btn.danger:hover { border-color: #ef4444; background: rgba(239,68,68,.15); color: #fff; }
154
+
155
+ /* List + text (keep sizes) */
156
+ .fm-list { padding: 4px 0; }
157
+ .fm-left { display: flex; flex-direction: column; min-width: 0; }
158
+ .fm-name { font-weight: 600; color: #e5e7eb; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
159
+ .fm-date { font-size: 11px; color: #9ca3af; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
160
+
161
+ /* Icons view */
162
+ .fm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px; padding: 6px; }
163
+ .fm-item { position: relative; display: flex; align-items: center; justify-content: center; padding: 8px; border: 1px solid #1f2937; border-radius: 8px; background: transparent; transition: background-color .12s ease, border-color .12s ease; }
164
+ .fm-item:hover { background: #0f172a; border-color: #334155; }
165
+ .fm-item .fm-thumb { width: 60px; height: 60px; border: 1px solid #1f2937; background: #0b0e14; border-radius: 6px; }
166
+ .fm-item .fm-del { position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; padding: 0; line-height: 1; }
167
+ `;
168
+ document.head.appendChild(style);
169
+ }
170
+
171
+ _buildUI() {
172
+ // Header: name input + Save + New
173
+ const header = document.createElement('div');
174
+ header.className = 'fm-row header';
175
+
176
+ this.nameInput = document.createElement('input');
177
+ this.nameInput.type = 'text';
178
+ this.nameInput.placeholder = 'Model name';
179
+ this.nameInput.value = this.currentName;
180
+ this.nameInput.className = 'fm-input fm-grow';
181
+ header.appendChild(this.nameInput);
182
+
183
+ // View toggle: list ↔ icons-only
184
+ this.viewToggleBtn = document.createElement('button');
185
+ this.viewToggleBtn.className = 'fm-btn';
186
+ this.viewToggleBtn.addEventListener('click', () => this.toggleViewMode());
187
+ header.appendChild(this.viewToggleBtn);
188
+
189
+ const saveBtn = document.createElement('button');
190
+ saveBtn.textContent = 'Save';
191
+ saveBtn.className = 'fm-btn';
192
+ saveBtn.addEventListener('click', () => this.saveCurrent());
193
+ header.appendChild(saveBtn);
194
+
195
+
196
+
197
+ const newBtn = document.createElement('button');
198
+ newBtn.textContent = 'New';
199
+ newBtn.className = 'fm-btn';
200
+ newBtn.addEventListener('click', () => this.newModel());
201
+ header.appendChild(newBtn);
202
+
203
+ this.uiElement.appendChild(header);
204
+
205
+ // List container
206
+ this.listEl = document.createElement('div');
207
+ this.listEl.className = 'fm-list';
208
+ this.uiElement.appendChild(this.listEl);
209
+
210
+ this._updateViewToggleUI();
211
+ }
212
+
213
+ // ----- Actions -----
214
+ async newModel() {
215
+ if (!this.viewer || !this.viewer.partHistory) return;
216
+ const proceed = await confirm('Clear current model and start a new one?');
217
+ if (!proceed) return;
218
+ await this.viewer.partHistory.reset();
219
+ this.viewer.partHistory.currentHistoryStepId = null;
220
+ await this.viewer.partHistory.runHistory();
221
+ this.currentName = '';
222
+ this.nameInput.value = '';
223
+ this._refreshHistoryCollections('new-model');
224
+ }
225
+
226
+ async saveCurrent() {
227
+ if (!this.viewer || !this.viewer.partHistory) return;
228
+ let name = (this.nameInput.value || '').trim();
229
+ if (!name) {
230
+ name = await prompt('Enter a name for this model:') || '';
231
+ name = name.trim();
232
+ if (!name) return;
233
+ this.nameInput.value = name;
234
+ }
235
+
236
+ // Get feature history JSON (now includes PMI views) and embed into a 3MF archive as Metadata/featureHistory.json
237
+ const jsonString = await this.viewer.partHistory.toJSON();
238
+ let additionalFiles = undefined;
239
+ let modelMetadata = undefined;
240
+ if (jsonString) {
241
+ additionalFiles = { 'Metadata/featureHistory.json': jsonString };
242
+ modelMetadata = { featureHistoryPath: '/Metadata/featureHistory.json' };
243
+ }
244
+ // Embed PMI view images under /views
245
+ try {
246
+ const viewFiles = await this.viewer?.pmiViewsWidget?.captureViewImagesForPackage?.();
247
+ if (viewFiles && typeof viewFiles === 'object') {
248
+ additionalFiles = { ...(additionalFiles || {}), ...viewFiles };
249
+ }
250
+ } catch (err) {
251
+ console.error('Failed to embed PMI view images:', err);
252
+ }
253
+ // Capture a 60x60 thumbnail of the current view
254
+ let thumbnail = null;
255
+ try {
256
+ thumbnail = await this._captureThumbnail(60);
257
+ } catch { /* ignore thumbnail failures */ }
258
+
259
+ // Generate a compact 3MF. For local storage we only need history (no meshes), but we do embed a thumbnail.
260
+ const threeMfBytes = await generate3MF([], { unit: 'millimeter', precision: 6, scale: 1, additionalFiles, modelMetadata, thumbnail });
261
+ const threeMfB64 = uint8ArrayToBase64(threeMfBytes);
262
+ const now = new Date().toISOString();
263
+
264
+ // Store only the 3MF (with embedded thumbnail) and timestamp
265
+ const record = { savedAt: now, data3mf: threeMfB64 };
266
+ if (thumbnail) record.thumbnail = thumbnail;
267
+ this._setModel(name, record);
268
+ // Update in-memory thumbnail cache so UI reflects the new preview immediately
269
+ try { if (thumbnail) this._thumbCache.set(name, thumbnail); } catch { }
270
+ this.currentName = name;
271
+ this._saveLastName(name);
272
+ this.refreshList();
273
+ }
274
+
275
+ async loadModel(name) {
276
+ if (!this.viewer || !this.viewer.partHistory) return;
277
+ const seq = ++this._loadSeq; // only the last call should win
278
+ const rec = this._getModel(name);
279
+ if (!rec) return alert('Model not found.');
280
+ await this.viewer.partHistory.reset();
281
+ // Prefer new 3MF-based storage
282
+ if (rec.data3mf && typeof rec.data3mf === 'string') {
283
+ try {
284
+ let b64 = rec.data3mf;
285
+ if (b64.startsWith('data:') && b64.includes(';base64,')) {
286
+ b64 = b64.split(';base64,')[1];
287
+ }
288
+ const bytes = base64ToUint8Array(b64);
289
+ // Try to extract feature history from 3MF
290
+ const zip = await JSZip.loadAsync(bytes.buffer);
291
+ const files = {};
292
+ Object.keys(zip.files || {}).forEach(p => files[p.toLowerCase()] = p);
293
+ let fhKey = files['metadata/featurehistory.json'];
294
+ if (!fhKey) {
295
+ for (const k of Object.keys(files)) { if (k.endsWith('featurehistory.json')) { fhKey = files[k]; break; } }
296
+ }
297
+ if (fhKey) {
298
+ const jsonData = await zip.file(fhKey).async('string');
299
+ let root = null;
300
+ try { root = JSON.parse(jsonData); } catch { }
301
+ // Ensure expressions is a string if present
302
+ if (root && root.expressions != null && typeof root.expressions !== 'string') {
303
+ try { root.expressions = String(root.expressions); } catch { root.expressions = String(root.expressions); }
304
+ }
305
+ if (root) {
306
+ await this.viewer.partHistory.fromJSON(JSON.stringify(root));
307
+ // Sync Expressions UI with imported code
308
+ try { if (this.viewer?.expressionsManager?.textArea) this.viewer.expressionsManager.textArea.value = this.viewer.partHistory.expressions || ''; } catch { }
309
+
310
+ // Refresh PMI views widget from PartHistory
311
+ try {
312
+ if (this.viewer?.pmiViewsWidget) {
313
+ this.viewer.pmiViewsWidget.refreshFromHistory?.();
314
+ this.viewer.pmiViewsWidget._renderList?.();
315
+ }
316
+ } catch { }
317
+
318
+ if (seq !== this._loadSeq) return;
319
+ this.currentName = name;
320
+ this.nameInput.value = name;
321
+ this._saveLastName(name);
322
+ await this.viewer.partHistory.runHistory();
323
+ this._refreshHistoryCollections('load-model');
324
+ return;
325
+ }
326
+ }
327
+ // No feature history found → fallback to import raw 3MF as mesh via Import3D feature
328
+ try {
329
+ const feat = await this.viewer?.partHistory?.newFeature?.('IMPORT3D');
330
+ if (feat) {
331
+ feat.inputParams.fileToImport = bytes.buffer; // Import3dModelFeature can auto-detect 3MF zip
332
+ feat.inputParams.deflectionAngle = 15;
333
+ feat.inputParams.centerMesh = true;
334
+ }
335
+ await this.viewer?.partHistory?.runHistory?.();
336
+ this._refreshHistoryCollections('load-model');
337
+ if (seq !== this._loadSeq) return;
338
+ this.currentName = name;
339
+ this.nameInput.value = name;
340
+ this._saveLastName(name);
341
+ return;
342
+ } catch { }
343
+ } catch (e) {
344
+ console.warn('[FileManagerWidget] Failed to load 3MF from storage; falling back to JSON if present.', e);
345
+ }
346
+ }
347
+ // JSON fallback path
348
+ try {
349
+ const payload = (typeof rec.data === 'string') ? rec.data : JSON.stringify(rec.data);
350
+ await this.viewer.partHistory.fromJSON(payload);
351
+ // Sync Expressions UI with imported code
352
+ try { if (this.viewer?.expressionsManager?.textArea) this.viewer.expressionsManager.textArea.value = this.viewer.partHistory.expressions || ''; } catch { }
353
+ } catch (e) {
354
+ alert('Failed to load model (invalid data).');
355
+ console.error(e);
356
+ return;
357
+ }
358
+ if (seq !== this._loadSeq) return;
359
+ this.currentName = name;
360
+ this.nameInput.value = name;
361
+ this._saveLastName(name);
362
+ await this.viewer.partHistory.runHistory();
363
+ this._refreshHistoryCollections('load-model');
364
+ }
365
+
366
+ deleteModel(name) {
367
+ const rec = this._getModel(name);
368
+ if (!rec) return;
369
+ const proceed = confirm(`Delete model "${name}"? This cannot be undone.`);
370
+ if (!proceed) return;
371
+ this._removeModel(name);
372
+ if (this.currentName === name) {
373
+ this.currentName = '';
374
+ if (this.nameInput.value === name) this.nameInput.value = '';
375
+ }
376
+ this.refreshList();
377
+ }
378
+
379
+ _refreshHistoryCollections(reason = 'manual') {
380
+ const detail = { source: 'file-manager', reason };
381
+ try {
382
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
383
+ const evt = (typeof CustomEvent === 'function')
384
+ ? new CustomEvent(HISTORY_COLLECTION_REFRESH_EVENT, { detail })
385
+ : null;
386
+ if (evt) window.dispatchEvent(evt);
387
+ else window.dispatchEvent({ type: HISTORY_COLLECTION_REFRESH_EVENT, detail });
388
+ }
389
+ } catch { /* ignore */ }
390
+
391
+ try { this.viewer?.historyWidget?.render?.(); } catch { }
392
+ try { this.viewer?.assemblyConstraintsWidget?.render?.(); } catch { }
393
+ try {
394
+ if (this.viewer?.pmiViewsWidget) {
395
+ this.viewer.pmiViewsWidget.refreshFromHistory?.();
396
+ this.viewer.pmiViewsWidget._renderList?.();
397
+ }
398
+ } catch { /* ignore */ }
399
+ }
400
+
401
+ refreshList() {
402
+ const items = this._listModels();
403
+ while (this.listEl.firstChild) this.listEl.removeChild(this.listEl.firstChild);
404
+
405
+ if (!items.length) {
406
+ const empty = document.createElement('div');
407
+ empty.className = 'fm-row';
408
+ empty.textContent = 'No saved models yet.';
409
+ this.listEl.appendChild(empty);
410
+ return;
411
+ }
412
+
413
+ const sorted = items.slice().sort((a, b) => String(b.savedAt).localeCompare(String(a.savedAt)));
414
+ if (this._iconsOnly) {
415
+ this._renderIconsView(sorted);
416
+ return;
417
+ }
418
+
419
+ for (const it of sorted) {
420
+ const row = document.createElement('div');
421
+ row.className = 'fm-row';
422
+
423
+ const thumb = document.createElement('img');
424
+ thumb.className = 'fm-thumb';
425
+ thumb.alt = `${it.name} thumbnail`;
426
+ this._applyThumbnailToImg(it, thumb);
427
+ thumb.addEventListener('click', () => this.loadModel(it.name));
428
+ row.appendChild(thumb);
429
+
430
+ const left = document.createElement('div');
431
+ left.className = 'fm-left fm-grow';
432
+ const nameDiv = document.createElement('div');
433
+ nameDiv.className = 'fm-name';
434
+ nameDiv.textContent = it.name;
435
+ nameDiv.addEventListener('click', () => this.loadModel(it.name));
436
+ left.appendChild(nameDiv);
437
+ const dt = new Date(it.savedAt);
438
+ const dateEl = document.createElement('div');
439
+ dateEl.className = 'fm-date';
440
+ dateEl.textContent = isNaN(dt) ? String(it.savedAt || '') : dt.toLocaleString();
441
+ left.appendChild(dateEl);
442
+ row.appendChild(left);
443
+
444
+ const openBtn = document.createElement('button');
445
+ openBtn.type = 'button';
446
+ openBtn.className = 'fm-btn';
447
+ openBtn.textContent = '📂';
448
+ openBtn.addEventListener('click', () => this.loadModel(it.name));
449
+ row.appendChild(openBtn);
450
+
451
+ const delBtn = document.createElement('button');
452
+ delBtn.type = 'button';
453
+ delBtn.className = 'fm-btn danger';
454
+ delBtn.textContent = '✕';
455
+ delBtn.addEventListener('click', () => this.deleteModel(it.name));
456
+ row.appendChild(delBtn);
457
+
458
+ this.listEl.appendChild(row);
459
+ }
460
+ }
461
+
462
+ toggleViewMode() {
463
+ this._iconsOnly = !this._iconsOnly;
464
+ this._saveIconsPref(this._iconsOnly);
465
+ this._updateViewToggleUI();
466
+ this.refreshList();
467
+ }
468
+ _updateViewToggleUI() {
469
+ if (!this.viewToggleBtn) return;
470
+ if (this._iconsOnly) {
471
+ this.viewToggleBtn.textContent = '☰';
472
+ this.viewToggleBtn.title = 'Switch to list view';
473
+ } else {
474
+ this.viewToggleBtn.textContent = '🔳';
475
+ this.viewToggleBtn.title = 'Switch to icons view';
476
+ }
477
+ }
478
+
479
+ _renderIconsView(items) {
480
+ const grid = document.createElement('div');
481
+ grid.className = 'fm-grid';
482
+ this.listEl.appendChild(grid);
483
+
484
+ for (const it of items) {
485
+ const cell = document.createElement('div');
486
+ cell.className = 'fm-item';
487
+ const dt = new Date(it.savedAt);
488
+ cell.title = `${it.name}\n${isNaN(dt) ? String(it.savedAt || '') : dt.toLocaleString()}`;
489
+ cell.addEventListener('click', () => this.loadModel(it.name));
490
+
491
+ const img = document.createElement('img');
492
+ img.className = 'fm-thumb';
493
+ img.alt = `${it.name} thumbnail`;
494
+ this._applyThumbnailToImg(it, img);
495
+ cell.appendChild(img);
496
+
497
+ const del = document.createElement('button');
498
+ del.type = 'button';
499
+ del.className = 'fm-btn danger fm-del';
500
+ del.textContent = '✕';
501
+ del.title = `Delete ${it.name}`;
502
+ del.addEventListener('click', (ev) => {
503
+ ev.stopPropagation();
504
+ this.deleteModel(it.name);
505
+ });
506
+ cell.appendChild(del);
507
+
508
+ grid.appendChild(cell);
509
+ }
510
+ }
511
+ async _applyThumbnailToImg(rec, imgEl) {
512
+ try {
513
+ if (!imgEl) return;
514
+ if (!rec?.data3mf) {
515
+ imgEl.style.display = 'none';
516
+ return;
517
+ }
518
+ imgEl.style.display = '';
519
+ if (rec.thumbnail) {
520
+ imgEl.src = rec.thumbnail;
521
+ if (this._thumbCache) this._thumbCache.set(rec.name, rec.thumbnail);
522
+ return;
523
+ }
524
+ if (this._thumbCache && this._thumbCache.has(rec.name)) {
525
+ const cached = this._thumbCache.get(rec.name);
526
+ if (cached) imgEl.src = cached;
527
+ return;
528
+ }
529
+ const src = await extractThumbnailFrom3MFBase64(rec.data3mf);
530
+ if (src) {
531
+ imgEl.src = src;
532
+ if (this._thumbCache) this._thumbCache.set(rec.name, src);
533
+ this._persistThumbnail(rec.name, src);
534
+ } else {
535
+ imgEl.style.display = 'none';
536
+ }
537
+ } catch {
538
+ if (imgEl) imgEl.style.display = 'none';
539
+ }
540
+ }
541
+
542
+ _persistThumbnail(name, thumbnail) {
543
+ if (!name || !thumbnail) return;
544
+ const existing = getComponentRecord(name);
545
+ if (!existing) return;
546
+ const payload = {
547
+ savedAt: existing.savedAt || new Date().toISOString(),
548
+ data3mf: existing.data3mf,
549
+ data: existing.data,
550
+ thumbnail,
551
+ };
552
+ setComponentRecord(name, payload);
553
+ }
554
+
555
+ async _captureThumbnail(size = 60) {
556
+ try {
557
+ const renderer = this.viewer?.renderer;
558
+ const canvas = renderer?.domElement;
559
+ const cam = this.viewer?.camera;
560
+ const controls = this.viewer?.controls;
561
+ if (!canvas || !cam) return null;
562
+
563
+ // Temporarily reorient exactly like clicking the ViewCube corner (top-front-right)
564
+ try {
565
+ const dir = new THREE.Vector3(1, 1, 1); // matches TOP FRONT RIGHT corner
566
+ if (this.viewer?.viewCube && typeof this.viewer.viewCube._reorientCamera === 'function') {
567
+ this.viewer.viewCube._reorientCamera(dir, 'SAVE THUMBNAIL');
568
+ } else {
569
+ // Fallback: replicate ViewCube corner logic if widget unavailable
570
+ const pivot = (controls && controls._gizmos && controls._gizmos.position)
571
+ ? controls._gizmos.position.clone()
572
+ : new THREE.Vector3(0, 0, 0);
573
+ const dist = cam.position.distanceTo(pivot) || cam.position.length() || 10;
574
+ const pos = pivot.clone().add(dir.clone().normalize().multiplyScalar(dist));
575
+ const useZup = Math.abs(dir.y) > 0.9;
576
+ const up = useZup ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0);
577
+ cam.position.copy(pos);
578
+ cam.up.copy(up);
579
+ cam.lookAt(pivot);
580
+ cam.updateMatrixWorld(true);
581
+ if (controls?.updateMatrixState) { try { controls.updateMatrixState(); } catch { } }
582
+ }
583
+ // Fit geometry within this oriented view
584
+ try { this.viewer.zoomToFit(1.1); } catch { }
585
+ } catch { /* ignore orientation failures */ }
586
+
587
+ // Ensure a fresh frame before capture
588
+ try { this.viewer.render(); } catch { }
589
+
590
+ // Wait one frame to be safe
591
+ await new Promise((resolve) => requestAnimationFrame(resolve));
592
+
593
+ const srcW = canvas.width || canvas.clientWidth || 1;
594
+ const srcH = canvas.height || canvas.clientHeight || 1;
595
+ const dst = document.createElement('canvas');
596
+ dst.width = size; dst.height = size;
597
+ const ctx = dst.getContext('2d');
598
+ if (!ctx) return null;
599
+ // Leave background transparent so captures can be composited cleanly
600
+ try { ctx.clearRect(0, 0, size, size); } catch { }
601
+ // Compute contain fit
602
+ const scale = Math.min(size / srcW, size / srcH);
603
+ const dw = Math.max(1, Math.floor(srcW * scale));
604
+ const dh = Math.max(1, Math.floor(srcH * scale));
605
+ const dx = Math.floor((size - dw) / 2);
606
+ const dy = Math.floor((size - dh) / 2);
607
+ try { ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; } catch { }
608
+ ctx.drawImage(canvas, 0, 0, srcW, srcH, dx, dy, dw, dh);
609
+ const dataUrl = dst.toDataURL('image/png');
610
+ return dataUrl;
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+ }