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.
- package/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- 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
|
+
}
|