@nice2dev/ui-3d 1.2.0 → 1.2.2
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/CHANGELOG.md +142 -0
- package/dist/cjs/core/editorAsk.js +48 -0
- package/dist/cjs/core/i18n.js +7 -3
- package/dist/cjs/dance/DanceScoreEngine.js +7 -0
- package/dist/cjs/dance/PoseDetector.js +4 -2
- package/dist/cjs/material/NiceMaterialEditor.js +8 -3
- package/dist/cjs/material/materialEditorTypes.js +1 -1
- package/dist/cjs/material/materialNodeDefinitions.js +14 -11
- package/dist/cjs/model/ModelEditor.js +9 -2
- package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
- package/dist/cjs/model/ModelEditorMenuBar.js +10 -8
- package/dist/cjs/model/ModelEditorRightPanel.js +28 -22
- package/dist/cjs/model/ModelEditorSubComponents.js +17 -9
- package/dist/cjs/model/ModelEditorTimeline.js +8 -4
- package/dist/cjs/model/ModelEditorToolbar.js +6 -4
- package/dist/cjs/model/ModelEditorViewport.js +19 -6
- package/dist/cjs/model/ModelViewer.js +9 -7
- package/dist/cjs/model/NiceArmatureEditor.js +9 -7
- package/dist/cjs/model/NiceMorphTargetEditor.js +7 -3
- package/dist/cjs/model/NiceOctree.js +9 -4
- package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
- package/dist/cjs/model/NiceProceduralGeometry.js +6 -4
- package/dist/cjs/model/NiceTerrainEditor.js +9 -7
- package/dist/cjs/model/NiceWeightPainter.js +6 -4
- package/dist/cjs/model/NiceXRPreview.js +9 -4
- package/dist/cjs/model/useModelEditor.js +113 -87
- package/dist/cjs/model/useModelViewer.js +13 -6
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +23 -11
- package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
- package/dist/cjs/rendering/NiceRenderExport.js +9 -3
- package/dist/cjs/rendering/NiceSSAO.js +4 -1
- package/dist/cjs/rendering/NiceSSR.js +4 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +5 -2
- package/dist/cjs/uv/NiceUVEditor.js +16 -12
- package/dist/cjs/uv/uvEditorTypes.js +1 -0
- package/dist/esm/core/editorAsk.js +45 -0
- package/dist/esm/core/i18n.js +8 -4
- package/dist/esm/dance/DanceScoreEngine.js +7 -0
- package/dist/esm/dance/PoseDetector.js +4 -2
- package/dist/esm/material/NiceMaterialEditor.js +8 -3
- package/dist/esm/material/materialEditorTypes.js +1 -1
- package/dist/esm/material/materialNodeDefinitions.js +14 -11
- package/dist/esm/model/ModelEditor.js +9 -2
- package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
- package/dist/esm/model/ModelEditorMenuBar.js +10 -8
- package/dist/esm/model/ModelEditorRightPanel.js +28 -22
- package/dist/esm/model/ModelEditorSubComponents.js +17 -9
- package/dist/esm/model/ModelEditorTimeline.js +8 -4
- package/dist/esm/model/ModelEditorToolbar.js +6 -4
- package/dist/esm/model/ModelEditorViewport.js +19 -6
- package/dist/esm/model/ModelViewer.js +9 -7
- package/dist/esm/model/NiceArmatureEditor.js +9 -7
- package/dist/esm/model/NiceMorphTargetEditor.js +7 -3
- package/dist/esm/model/NiceOctree.js +9 -4
- package/dist/esm/model/NicePhysicsSimulation.js +7 -5
- package/dist/esm/model/NiceProceduralGeometry.js +6 -4
- package/dist/esm/model/NiceTerrainEditor.js +9 -7
- package/dist/esm/model/NiceWeightPainter.js +6 -4
- package/dist/esm/model/NiceXRPreview.js +9 -4
- package/dist/esm/model/useModelEditor.js +113 -87
- package/dist/esm/model/useModelViewer.js +13 -6
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +23 -11
- package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
- package/dist/esm/rendering/NiceRenderExport.js +9 -3
- package/dist/esm/rendering/NiceSSAO.js +4 -1
- package/dist/esm/rendering/NiceSSR.js +4 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +5 -2
- package/dist/esm/uv/NiceUVEditor.js +16 -12
- package/dist/esm/uv/uvEditorTypes.js +1 -0
- package/dist/types/core/editorAsk.d.ts +26 -0
- package/dist/types/core/i18n.d.ts +1 -17
- package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
- package/dist/types/model/useModelEditor.d.ts +3 -0
- package/package.json +3 -3
|
@@ -21,6 +21,8 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
|
21
21
|
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
|
|
22
22
|
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
|
|
23
23
|
import { VTKLoader } from 'three/examples/jsm/loaders/VTKLoader.js';
|
|
24
|
+
import { useEditorAsk } from '../core/editorAsk.js';
|
|
25
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
24
26
|
import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
|
|
25
27
|
import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
|
|
26
28
|
import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
|
|
@@ -32,7 +34,7 @@ import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstance
|
|
|
32
34
|
*
|
|
33
35
|
* Extracted from the original ModelEditor.tsx god-component.
|
|
34
36
|
*/
|
|
35
|
-
const log = { error: console.error, warn: console.warn
|
|
37
|
+
const log = { error: console.error, warn: console.warn };
|
|
36
38
|
/* helpers */
|
|
37
39
|
let _nid = 0;
|
|
38
40
|
const nid = () => `n${++_nid}`;
|
|
@@ -54,6 +56,8 @@ function findNodeById(nodes, id) {
|
|
|
54
56
|
═══════════════════════════════════════════ */
|
|
55
57
|
function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
56
58
|
var _a;
|
|
59
|
+
const { t } = useNiceTranslation();
|
|
60
|
+
const { pendingAsk, ask } = useEditorAsk();
|
|
57
61
|
/* ── refs ── */
|
|
58
62
|
const mountRef = useRef(null);
|
|
59
63
|
const rendererRef = useRef(null);
|
|
@@ -204,10 +208,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
204
208
|
// goes into the scene is its helper.
|
|
205
209
|
const tcHelper = tc.getHelper();
|
|
206
210
|
tcHelper.name = '__transformControls';
|
|
207
|
-
|
|
208
|
-
tc.addEventListener('dragging-changed', ((e) => {
|
|
211
|
+
tc.addEventListener('dragging-changed', (e) => {
|
|
209
212
|
orbit.enabled = !e.value;
|
|
210
|
-
})
|
|
213
|
+
});
|
|
211
214
|
scene.add(tcHelper);
|
|
212
215
|
transformRef.current = tc;
|
|
213
216
|
// Resize handler
|
|
@@ -393,14 +396,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
393
396
|
}
|
|
394
397
|
focusOnObjectRef.current(obj);
|
|
395
398
|
rebuildTree();
|
|
396
|
-
setStatusText(
|
|
397
|
-
|
|
399
|
+
setStatusText(t('useModelEditor.loadedAnimationS', 'Loaded: {filename} ({clips} animation(s))')
|
|
400
|
+
.replace('{filename}', String(filename))
|
|
401
|
+
.replace('{clips}', String(clips.length)));
|
|
402
|
+
}, [rebuildTree, t]);
|
|
398
403
|
const loadFile = useCallback(async (file) => {
|
|
399
404
|
var _a, _b, _c, _d, _e, _f;
|
|
400
405
|
const name = file.name.toLowerCase();
|
|
401
406
|
const ext = '.' + name.split('.').pop();
|
|
402
407
|
const url = URL.createObjectURL(file);
|
|
403
|
-
setStatusText(
|
|
408
|
+
setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
|
|
404
409
|
try {
|
|
405
410
|
if (ext === '.fbx') {
|
|
406
411
|
const loader = new FBXLoader();
|
|
@@ -524,32 +529,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
524
529
|
addObjectToScene(group, [], file.name);
|
|
525
530
|
}
|
|
526
531
|
else {
|
|
527
|
-
setStatusText(
|
|
532
|
+
setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
|
|
528
533
|
}
|
|
529
534
|
}
|
|
530
535
|
catch (err) {
|
|
531
536
|
log.error('Load error:', err);
|
|
532
|
-
setStatusText(
|
|
537
|
+
setStatusText(t('useModelEditor.errorLoading', 'Error loading {name}: {err}')
|
|
538
|
+
.replace('{name}', String(file.name))
|
|
539
|
+
.replace('{err}', String(String(err))));
|
|
533
540
|
}
|
|
534
541
|
finally {
|
|
535
542
|
URL.revokeObjectURL(url);
|
|
536
543
|
}
|
|
537
|
-
}, [addObjectToScene]);
|
|
544
|
+
}, [addObjectToScene, t]);
|
|
538
545
|
const mergeAnimationFBX = useCallback(async (file) => {
|
|
539
546
|
var _a;
|
|
540
547
|
const url = URL.createObjectURL(file);
|
|
541
|
-
setStatusText(
|
|
548
|
+
setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
|
|
542
549
|
try {
|
|
543
550
|
const loader = new FBXLoader();
|
|
544
551
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
545
552
|
const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
|
|
546
553
|
if (clips.length === 0) {
|
|
547
|
-
setStatusText(
|
|
554
|
+
setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
|
|
548
555
|
return;
|
|
549
556
|
}
|
|
550
557
|
const root = rootObjectRef.current;
|
|
551
558
|
if (!root) {
|
|
552
|
-
setStatusText('No model loaded — load a base model first');
|
|
559
|
+
setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
|
|
553
560
|
return;
|
|
554
561
|
}
|
|
555
562
|
if (!mixerRef.current) {
|
|
@@ -563,16 +570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
563
570
|
}));
|
|
564
571
|
allClipsRef.current = [...allClipsRef.current, ...newAnims];
|
|
565
572
|
setAnimations([...allClipsRef.current]);
|
|
566
|
-
setStatusText(
|
|
573
|
+
setStatusText(t('useModelEditor.mergedAnimationSFrom', 'Merged {clips} animation(s) from {name}')
|
|
574
|
+
.replace('{clips}', String(clips.length))
|
|
575
|
+
.replace('{name}', String(file.name)));
|
|
567
576
|
}
|
|
568
577
|
catch (err) {
|
|
569
578
|
log.error('Merge error:', err);
|
|
570
|
-
setStatusText(
|
|
579
|
+
setStatusText(t('useModelEditor.errorMerging', 'Error merging {name}: {err}')
|
|
580
|
+
.replace('{name}', String(file.name))
|
|
581
|
+
.replace('{err}', String(String(err))));
|
|
571
582
|
}
|
|
572
583
|
finally {
|
|
573
584
|
URL.revokeObjectURL(url);
|
|
574
585
|
}
|
|
575
|
-
}, []);
|
|
586
|
+
}, [t]);
|
|
576
587
|
/* ─────────────────────────────────────────
|
|
577
588
|
Animation controls
|
|
578
589
|
───────────────────────────────────────── */
|
|
@@ -930,8 +941,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
930
941
|
clone.position.x += 1;
|
|
931
942
|
sceneRef.current.add(clone);
|
|
932
943
|
rebuildTree();
|
|
933
|
-
setStatusText(
|
|
934
|
-
}, [selectedNode, rebuildTree]);
|
|
944
|
+
setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
|
|
945
|
+
}, [selectedNode, rebuildTree, t]);
|
|
935
946
|
/* ─────────────────────────────────────────
|
|
936
947
|
Keyboard shortcuts
|
|
937
948
|
───────────────────────────────────────── */
|
|
@@ -1075,7 +1086,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1075
1086
|
const exportGLTF = useCallback((binary) => {
|
|
1076
1087
|
const root = rootObjectRef.current;
|
|
1077
1088
|
if (!root) {
|
|
1078
|
-
setStatusText('Nothing to export');
|
|
1089
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1079
1090
|
return;
|
|
1080
1091
|
}
|
|
1081
1092
|
const exporter = new GLTFExporter();
|
|
@@ -1096,16 +1107,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1096
1107
|
a.download = `export.${binary ? 'glb' : 'gltf'}`;
|
|
1097
1108
|
a.click();
|
|
1098
1109
|
URL.revokeObjectURL(a.href);
|
|
1099
|
-
setStatusText(
|
|
1110
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? 'GLB' : 'GLTF'));
|
|
1100
1111
|
}, (err) => {
|
|
1101
1112
|
log.error('Export error:', err);
|
|
1102
|
-
setStatusText(
|
|
1113
|
+
setStatusText(t('useModelEditor.exportError', 'Export error: {err}').replace('{err}', String(String(err))));
|
|
1103
1114
|
}, options);
|
|
1104
|
-
}, []);
|
|
1115
|
+
}, [t]);
|
|
1105
1116
|
const exportOBJ = useCallback(() => {
|
|
1106
1117
|
const root = rootObjectRef.current;
|
|
1107
1118
|
if (!root) {
|
|
1108
|
-
setStatusText('Nothing to export');
|
|
1119
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1109
1120
|
return;
|
|
1110
1121
|
}
|
|
1111
1122
|
const exporter = new OBJExporter();
|
|
@@ -1116,12 +1127,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1116
1127
|
a.download = 'export.obj';
|
|
1117
1128
|
a.click();
|
|
1118
1129
|
URL.revokeObjectURL(a.href);
|
|
1119
|
-
setStatusText('Exported
|
|
1120
|
-
}, []);
|
|
1130
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'OBJ'));
|
|
1131
|
+
}, [t]);
|
|
1121
1132
|
const exportSTL = useCallback((binary) => {
|
|
1122
1133
|
const root = rootObjectRef.current;
|
|
1123
1134
|
if (!root) {
|
|
1124
|
-
setStatusText('Nothing to export');
|
|
1135
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1125
1136
|
return;
|
|
1126
1137
|
}
|
|
1127
1138
|
const exporter = new STLExporter();
|
|
@@ -1138,12 +1149,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1138
1149
|
a.download = 'export.stl';
|
|
1139
1150
|
a.click();
|
|
1140
1151
|
URL.revokeObjectURL(a.href);
|
|
1141
|
-
setStatusText(
|
|
1142
|
-
}, []);
|
|
1152
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.stlBinary', 'STL (binary)') : 'STL'));
|
|
1153
|
+
}, [t]);
|
|
1143
1154
|
const exportPLY = useCallback((binary) => {
|
|
1144
1155
|
const root = rootObjectRef.current;
|
|
1145
1156
|
if (!root) {
|
|
1146
|
-
setStatusText('Nothing to export');
|
|
1157
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1147
1158
|
return;
|
|
1148
1159
|
}
|
|
1149
1160
|
const exporter = new PLYExporter();
|
|
@@ -1161,13 +1172,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1161
1172
|
a.download = 'export.ply';
|
|
1162
1173
|
a.click();
|
|
1163
1174
|
URL.revokeObjectURL(a.href);
|
|
1164
|
-
setStatusText(
|
|
1175
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.plyBinary', 'PLY (binary)') : 'PLY'));
|
|
1165
1176
|
}, { binary });
|
|
1166
|
-
}, []);
|
|
1177
|
+
}, [t]);
|
|
1167
1178
|
const exportUSDZ = useCallback(async () => {
|
|
1168
1179
|
const root = rootObjectRef.current;
|
|
1169
1180
|
if (!root) {
|
|
1170
|
-
setStatusText('Nothing to export');
|
|
1181
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1171
1182
|
return;
|
|
1172
1183
|
}
|
|
1173
1184
|
try {
|
|
@@ -1182,13 +1193,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1182
1193
|
a.download = 'export.usdz';
|
|
1183
1194
|
a.click();
|
|
1184
1195
|
URL.revokeObjectURL(a.href);
|
|
1185
|
-
setStatusText('Exported
|
|
1196
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'USDZ'));
|
|
1186
1197
|
}
|
|
1187
1198
|
catch (err) {
|
|
1188
1199
|
log.error('USDZ export error:', err);
|
|
1189
|
-
setStatusText(
|
|
1200
|
+
setStatusText(t('useModelEditor.usdzExportError', 'USDZ export error: {err}').replace('{err}', String(String(err))));
|
|
1190
1201
|
}
|
|
1191
|
-
}, []);
|
|
1202
|
+
}, [t]);
|
|
1192
1203
|
/* ─────────────────────────────────────────
|
|
1193
1204
|
Dispose & clear
|
|
1194
1205
|
───────────────────────────────────────── */
|
|
@@ -1226,11 +1237,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1226
1237
|
}
|
|
1227
1238
|
});
|
|
1228
1239
|
}, []);
|
|
1229
|
-
const
|
|
1240
|
+
const clearSceneNow = useCallback(() => {
|
|
1230
1241
|
var _a;
|
|
1231
|
-
if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
1242
|
const scene = sceneRef.current;
|
|
1235
1243
|
const toRemove = [];
|
|
1236
1244
|
scene.traverse((obj) => {
|
|
@@ -1260,8 +1268,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1260
1268
|
setAnimDuration(0);
|
|
1261
1269
|
setSelectedId(null);
|
|
1262
1270
|
rebuildTree();
|
|
1263
|
-
setStatusText('Scene cleared');
|
|
1264
|
-
}, [rebuildTree, disposeObject]);
|
|
1271
|
+
setStatusText(t('useModelEditor.sceneCleared', 'Scene cleared'));
|
|
1272
|
+
}, [rebuildTree, disposeObject, t]);
|
|
1273
|
+
// asks first: clearing cannot be undone
|
|
1274
|
+
const clearScene = useCallback(() => {
|
|
1275
|
+
void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
|
|
1276
|
+
if (answer !== null) {
|
|
1277
|
+
clearSceneNow();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}, [ask, clearSceneNow]);
|
|
1265
1281
|
/* ─────────────────────────────────────────
|
|
1266
1282
|
Save to Library
|
|
1267
1283
|
───────────────────────────────────────── */
|
|
@@ -1271,7 +1287,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1271
1287
|
}
|
|
1272
1288
|
const root = rootObjectRef.current;
|
|
1273
1289
|
if (!root) {
|
|
1274
|
-
setStatusText('Nothing to save');
|
|
1290
|
+
setStatusText(t('useModelEditor.nothingToSave', 'Nothing to save'));
|
|
1275
1291
|
return;
|
|
1276
1292
|
}
|
|
1277
1293
|
const exporter = new GLTFExporter();
|
|
@@ -1279,19 +1295,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1279
1295
|
const blob = new Blob([result], { type: 'model/gltf-binary' });
|
|
1280
1296
|
const reader = new FileReader();
|
|
1281
1297
|
reader.onload = () => {
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1298
|
+
const dataUrl = reader.result;
|
|
1299
|
+
void ask('prompt', 'Save to library as:', 'model.glb').then((name) => {
|
|
1300
|
+
if (name && name.trim()) {
|
|
1301
|
+
onSaveToLibrary(dataUrl, name.trim(), 'model/gltf-binary');
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1288
1304
|
};
|
|
1289
1305
|
reader.readAsDataURL(blob);
|
|
1290
1306
|
}, (err) => {
|
|
1291
1307
|
log.error('Save to library error:', err);
|
|
1292
|
-
setStatusText(
|
|
1308
|
+
setStatusText(t('useModelEditor.saveError', 'Save error: {err}').replace('{err}', String(String(err))));
|
|
1293
1309
|
}, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
|
|
1294
|
-
}, [onSaveToLibrary]);
|
|
1310
|
+
}, [onSaveToLibrary, ask, t]);
|
|
1295
1311
|
/* ─────────────────────────────────────────
|
|
1296
1312
|
Add Mesh / Light / Camera / Empty
|
|
1297
1313
|
───────────────────────────────────────── */
|
|
@@ -1358,9 +1374,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1358
1374
|
}
|
|
1359
1375
|
sceneRef.current.add(mesh);
|
|
1360
1376
|
rebuildTree();
|
|
1361
|
-
setStatusText(
|
|
1377
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(mesh.name)));
|
|
1362
1378
|
setAddMenuOpen(null);
|
|
1363
|
-
}, [rebuildTree]);
|
|
1379
|
+
}, [rebuildTree, t]);
|
|
1364
1380
|
const addSceneLight = useCallback((type) => {
|
|
1365
1381
|
let light;
|
|
1366
1382
|
switch (type) {
|
|
@@ -1412,9 +1428,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1412
1428
|
sceneRef.current.add(helper);
|
|
1413
1429
|
}
|
|
1414
1430
|
rebuildTree();
|
|
1415
|
-
setStatusText(
|
|
1431
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(light.name)));
|
|
1416
1432
|
setAddMenuOpen(null);
|
|
1417
|
-
}, [rebuildTree]);
|
|
1433
|
+
}, [rebuildTree, t]);
|
|
1418
1434
|
const addCameraObject = useCallback(() => {
|
|
1419
1435
|
const cam = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
|
|
1420
1436
|
cam.name = 'Camera';
|
|
@@ -1425,9 +1441,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1425
1441
|
helper.name = '__helper_Camera';
|
|
1426
1442
|
sceneRef.current.add(helper);
|
|
1427
1443
|
rebuildTree();
|
|
1428
|
-
setStatusText('Added Camera');
|
|
1444
|
+
setStatusText(t('useModelEditor.addedCamera', 'Added Camera'));
|
|
1429
1445
|
setAddMenuOpen(null);
|
|
1430
|
-
}, [rebuildTree]);
|
|
1446
|
+
}, [rebuildTree, t]);
|
|
1431
1447
|
const addEmpty = useCallback((type) => {
|
|
1432
1448
|
const obj = new THREE.Object3D();
|
|
1433
1449
|
obj.name =
|
|
@@ -1439,9 +1455,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1439
1455
|
obj.add(h);
|
|
1440
1456
|
}
|
|
1441
1457
|
rebuildTree();
|
|
1442
|
-
setStatusText(
|
|
1458
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(obj.name)));
|
|
1443
1459
|
setAddMenuOpen(null);
|
|
1444
|
-
}, [rebuildTree]);
|
|
1460
|
+
}, [rebuildTree, t]);
|
|
1445
1461
|
/* ─────────────────────────────────────────
|
|
1446
1462
|
Raycaster click-to-select
|
|
1447
1463
|
───────────────────────────────────────── */
|
|
@@ -1509,14 +1525,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1509
1525
|
}
|
|
1510
1526
|
e.target.value = '';
|
|
1511
1527
|
if (!onAIPoseVideo) {
|
|
1512
|
-
setAiStatus('AI pose callback not configured.');
|
|
1528
|
+
setAiStatus(t('useModelEditor.aiPoseCallbackNotConfigured', 'AI pose callback not configured.'));
|
|
1513
1529
|
return;
|
|
1514
1530
|
}
|
|
1515
1531
|
setAiBusy(true);
|
|
1516
|
-
setAiStatus('Uploading video for AI pose estimation...');
|
|
1532
|
+
setAiStatus(t('useModelEditor.uploadingVideoForAiPose', 'Uploading video for AI pose estimation...'));
|
|
1517
1533
|
try {
|
|
1518
1534
|
const result = await onAIPoseVideo(file);
|
|
1519
|
-
setAiStatus(
|
|
1535
|
+
setAiStatus(t('useModelEditor.gotFramesAtFpsCreating', 'Got {frame_count} frames at {fps} fps. Creating animation clip...')
|
|
1536
|
+
.replace('{frame_count}', String(result.frame_count))
|
|
1537
|
+
.replace('{fps}', String(result.fps)));
|
|
1520
1538
|
const clip = pose3dToClip(result);
|
|
1521
1539
|
if (clip && rootObjectRef.current) {
|
|
1522
1540
|
if (!mixerRef.current) {
|
|
@@ -1531,20 +1549,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1531
1549
|
allClipsRef.current = [...allClipsRef.current, newAnim];
|
|
1532
1550
|
setAnimations([...allClipsRef.current]);
|
|
1533
1551
|
playClip(allClipsRef.current.length - 1);
|
|
1534
|
-
setAiStatus('Animation created successfully!');
|
|
1552
|
+
setAiStatus(t('useModelEditor.animationCreatedSuccessfully', 'Animation created successfully!'));
|
|
1535
1553
|
}
|
|
1536
1554
|
else {
|
|
1537
|
-
setAiStatus('Could not create animation — load a rigged model first.');
|
|
1555
|
+
setAiStatus(t('useModelEditor.couldNotCreateAnimationLoad', 'Could not create animation — load a rigged model first.'));
|
|
1538
1556
|
}
|
|
1539
1557
|
}
|
|
1540
1558
|
catch (err) {
|
|
1541
1559
|
log.error('AI pose error:', err);
|
|
1542
|
-
setAiStatus(
|
|
1560
|
+
setAiStatus(t('useModelEditor.error', 'Error: {err}').replace('{err}', String(String(err))));
|
|
1543
1561
|
}
|
|
1544
1562
|
finally {
|
|
1545
1563
|
setAiBusy(false);
|
|
1546
1564
|
}
|
|
1547
|
-
}, [playClip, onAIPoseVideo]);
|
|
1565
|
+
}, [playClip, onAIPoseVideo, t]);
|
|
1548
1566
|
/* ─────────────────────────────────────────
|
|
1549
1567
|
Node helpers
|
|
1550
1568
|
───────────────────────────────────────── */
|
|
@@ -1568,19 +1586,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1568
1586
|
const undo = useCallback(() => {
|
|
1569
1587
|
const label = undoRedoRef.current.undo();
|
|
1570
1588
|
if (label) {
|
|
1571
|
-
setStatusText(
|
|
1589
|
+
setStatusText(t('useModelEditor.undo', 'Undo: {label}').replace('{label}', String(label)));
|
|
1572
1590
|
rebuildTree();
|
|
1573
1591
|
}
|
|
1574
1592
|
setUndoRedoVer((v) => v + 1);
|
|
1575
|
-
}, [rebuildTree]);
|
|
1593
|
+
}, [rebuildTree, t]);
|
|
1576
1594
|
const redo = useCallback(() => {
|
|
1577
1595
|
const label = undoRedoRef.current.redo();
|
|
1578
1596
|
if (label) {
|
|
1579
|
-
setStatusText(
|
|
1597
|
+
setStatusText(t('useModelEditor.redo', 'Redo: {label}').replace('{label}', String(label)));
|
|
1580
1598
|
rebuildTree();
|
|
1581
1599
|
}
|
|
1582
1600
|
setUndoRedoVer((v) => v + 1);
|
|
1583
|
-
}, [rebuildTree]);
|
|
1601
|
+
}, [rebuildTree, t]);
|
|
1584
1602
|
const canUndo = undoRedoRef.current.canUndo();
|
|
1585
1603
|
const canRedo = undoRedoRef.current.canRedo();
|
|
1586
1604
|
const undoLabel = undoRedoRef.current.undoLabel;
|
|
@@ -1609,30 +1627,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1609
1627
|
return SUPPORTED_EXTENSIONS.includes(ext);
|
|
1610
1628
|
});
|
|
1611
1629
|
if (fileArray.length === 0) {
|
|
1612
|
-
setStatusText('No supported files found');
|
|
1630
|
+
setStatusText(t('useModelEditor.noSupportedFilesFound', 'No supported files found'));
|
|
1613
1631
|
return;
|
|
1614
1632
|
}
|
|
1615
|
-
setStatusText(
|
|
1633
|
+
setStatusText(t('useModelEditor.importingFileS', 'Importing {fileArray} file(s)...').replace('{fileArray}', String(fileArray.length)));
|
|
1616
1634
|
let loaded = 0;
|
|
1617
1635
|
for (const file of fileArray) {
|
|
1618
1636
|
try {
|
|
1619
1637
|
await loadFile(file);
|
|
1620
1638
|
loaded++;
|
|
1621
|
-
setStatusText(
|
|
1639
|
+
setStatusText(t('useModelEditor.imported', 'Imported {loaded}/{fileArray}: {name}')
|
|
1640
|
+
.replace('{loaded}', String(loaded))
|
|
1641
|
+
.replace('{fileArray}', String(fileArray.length))
|
|
1642
|
+
.replace('{name}', String(file.name)));
|
|
1622
1643
|
}
|
|
1623
1644
|
catch (err) {
|
|
1624
1645
|
log.error(`Failed to import ${file.name}:`, err);
|
|
1625
1646
|
}
|
|
1626
1647
|
}
|
|
1627
|
-
setStatusText(
|
|
1628
|
-
|
|
1648
|
+
setStatusText(t('useModelEditor.batchImportCompleteFilesLoaded', 'Batch import complete: {loaded}/{fileArray} files loaded')
|
|
1649
|
+
.replace('{loaded}', String(loaded))
|
|
1650
|
+
.replace('{fileArray}', String(fileArray.length)));
|
|
1651
|
+
}, [loadFile, t]);
|
|
1629
1652
|
/* ─────────────────────────────────────────
|
|
1630
1653
|
PBR Presets
|
|
1631
1654
|
───────────────────────────────────────── */
|
|
1632
1655
|
const applyPreset = useCallback((preset) => {
|
|
1633
1656
|
const node = selectedNode;
|
|
1634
1657
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1635
|
-
setStatusText('Select a mesh to apply material preset');
|
|
1658
|
+
setStatusText(t('useModelEditor.selectAMeshToApply', 'Select a mesh to apply material preset'));
|
|
1636
1659
|
return;
|
|
1637
1660
|
}
|
|
1638
1661
|
const mesh = node.object;
|
|
@@ -1665,7 +1688,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1665
1688
|
mesh.material = newMaterial;
|
|
1666
1689
|
}
|
|
1667
1690
|
pushUndo({
|
|
1668
|
-
label:
|
|
1691
|
+
label: t('useModelEditor.applyPreset', 'Apply {name} preset').replace('{name}', String(preset.name)),
|
|
1669
1692
|
undo: () => {
|
|
1670
1693
|
if (Array.isArray(mesh.material)) {
|
|
1671
1694
|
mesh.material[materialIdx] = oldClone;
|
|
@@ -1684,15 +1707,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1684
1707
|
},
|
|
1685
1708
|
});
|
|
1686
1709
|
setMatRefresh((v) => v + 1);
|
|
1687
|
-
setStatusText(
|
|
1688
|
-
}, [selectedNode, selMaterialIdx, pushUndo]);
|
|
1710
|
+
setStatusText(t('useModelEditor.appliedPreset', 'Applied {name} preset').replace('{name}', String(preset.name)));
|
|
1711
|
+
}, [selectedNode, selMaterialIdx, pushUndo, t]);
|
|
1689
1712
|
/* ─────────────────────────────────────────
|
|
1690
1713
|
LOD Generation
|
|
1691
1714
|
───────────────────────────────────────── */
|
|
1692
1715
|
const generateSelectedLOD = useCallback(() => {
|
|
1693
1716
|
const node = selectedNode;
|
|
1694
1717
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1695
|
-
setStatusText('Select a mesh to generate LOD');
|
|
1718
|
+
setStatusText(t('useModelEditor.selectAMeshToGenerate', 'Select a mesh to generate LOD'));
|
|
1696
1719
|
return;
|
|
1697
1720
|
}
|
|
1698
1721
|
const mesh = node.object;
|
|
@@ -1701,7 +1724,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1701
1724
|
parent.remove(mesh);
|
|
1702
1725
|
parent.add(lod);
|
|
1703
1726
|
pushUndo({
|
|
1704
|
-
label:
|
|
1727
|
+
label: t('useModelEditor.generateLodFor', 'Generate LOD for {name}').replace('{name}', String(mesh.name)),
|
|
1705
1728
|
undo: () => {
|
|
1706
1729
|
parent.remove(lod);
|
|
1707
1730
|
parent.add(mesh);
|
|
@@ -1712,8 +1735,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1712
1735
|
},
|
|
1713
1736
|
});
|
|
1714
1737
|
rebuildTree();
|
|
1715
|
-
setStatusText(
|
|
1716
|
-
}, [selectedNode, pushUndo, rebuildTree]);
|
|
1738
|
+
setStatusText(t('useModelEditor.generatedLodFor4Levels', 'Generated LOD for {name} (4 levels)').replace('{name}', String(mesh.name)));
|
|
1739
|
+
}, [selectedNode, pushUndo, rebuildTree, t]);
|
|
1717
1740
|
/* ─────────────────────────────────────────
|
|
1718
1741
|
Instancing
|
|
1719
1742
|
───────────────────────────────────────── */
|
|
@@ -1721,12 +1744,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1721
1744
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1722
1745
|
const result = convertToInstanced(root);
|
|
1723
1746
|
if (result.created === 0) {
|
|
1724
|
-
setStatusText('No duplicate meshes found to instance');
|
|
1747
|
+
setStatusText(t('useModelEditor.noDuplicateMeshesFoundTo', 'No duplicate meshes found to instance'));
|
|
1725
1748
|
return;
|
|
1726
1749
|
}
|
|
1727
1750
|
rebuildTree();
|
|
1728
|
-
setStatusText(
|
|
1729
|
-
|
|
1751
|
+
setStatusText(t('useModelEditor.createdInstancedMeshesRemovedDuplicates', 'Created {created} instanced meshes, removed {removed} duplicates')
|
|
1752
|
+
.replace('{created}', String(result.created))
|
|
1753
|
+
.replace('{removed}', String(result.removed)));
|
|
1754
|
+
}, [rebuildTree, t]);
|
|
1730
1755
|
/* ── HDRI Environment ── */
|
|
1731
1756
|
const applyHDRI = useCallback((preset) => {
|
|
1732
1757
|
const renderer = rendererRef.current;
|
|
@@ -1744,8 +1769,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1744
1769
|
else {
|
|
1745
1770
|
setFogEnabled(false);
|
|
1746
1771
|
}
|
|
1747
|
-
setStatusText(
|
|
1748
|
-
}, []);
|
|
1772
|
+
setStatusText(t('useModelEditor.environment', 'Environment: {name}').replace('{name}', String(preset.name)));
|
|
1773
|
+
}, [t]);
|
|
1749
1774
|
/* ── Snap ── */
|
|
1750
1775
|
const snapPoint = useCallback((point) => {
|
|
1751
1776
|
const root = rootObjectRef.current || sceneRef.current;
|
|
@@ -1787,6 +1812,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1787
1812
|
bgColor,
|
|
1788
1813
|
setBgColor,
|
|
1789
1814
|
statusText,
|
|
1815
|
+
pendingAsk,
|
|
1790
1816
|
dragOver,
|
|
1791
1817
|
polyCount,
|
|
1792
1818
|
meshCount,
|
|
@@ -8,6 +8,7 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
|
8
8
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
9
9
|
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
|
|
10
10
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
11
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
11
12
|
import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -41,6 +42,7 @@ const TONE_MAP = {
|
|
|
41
42
|
═══════════════════════════════════════════ */
|
|
42
43
|
function useModelViewer(props = {}) {
|
|
43
44
|
var _a;
|
|
45
|
+
const { t } = useNiceTranslation();
|
|
44
46
|
const { src, format, navigationMode: initNavMode = 'orbit', quality = 'high', autoRotateSpeed: initAutoRotate = 0, backgroundColor = 'var(--nice-bg, #1a1a2e)', showGrid: initShowGrid = true, shadows = true, gamepad = false, cameraPresets: _cameraPresets = [], annotations: _annotations = [], hotspots: _hotspots = [], floorPlans: _floorPlans = [], onHotspotClick: _onHotspotClick, onAnnotationClick: _onAnnotationClick, onLoadProgress, onLoad, onError, clippingPlanes = [], measureMode: initMeasureMode = false, measurements: _measurements = [], onMeasure: _onMeasure, minDistance = 0.1, maxDistance = 1000, fov = 50, vr: _vr = false, walkSpeed = 5, ambientIntensity = 0.5, directionalIntensity = 0.8, antiAlias = true, toneMapping = 'aces', exposure = 1, } = props;
|
|
45
47
|
/* ── refs ── */
|
|
46
48
|
const mountRef = useRef(null);
|
|
@@ -328,7 +330,7 @@ function useModelViewer(props = {}) {
|
|
|
328
330
|
setLoading(true);
|
|
329
331
|
setLoadProgress(0);
|
|
330
332
|
setError(null);
|
|
331
|
-
setStatusText('Loading…');
|
|
333
|
+
setStatusText(t('useModelViewer.loading', 'Loading…'));
|
|
332
334
|
const detectedFmt = detectFormat(url, fmt);
|
|
333
335
|
const onProgress = (e) => {
|
|
334
336
|
if (e.lengthComputable) {
|
|
@@ -436,21 +438,23 @@ function useModelViewer(props = {}) {
|
|
|
436
438
|
mixerRef.current = mixer;
|
|
437
439
|
}
|
|
438
440
|
countScene();
|
|
439
|
-
setStatusText(
|
|
441
|
+
setStatusText(t('useModelViewer.loadedTrisMeshes', 'Loaded – {polyCount} tris, {meshCount} meshes')
|
|
442
|
+
.replace('{polyCount}', String(polyCount))
|
|
443
|
+
.replace('{meshCount}', String(meshCount)));
|
|
440
444
|
onLoad === null || onLoad === void 0 ? void 0 : onLoad();
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
catch (err) {
|
|
444
448
|
const msg = err instanceof Error ? err.message : String(err);
|
|
445
449
|
setError(msg);
|
|
446
|
-
setStatusText(
|
|
450
|
+
setStatusText(t('useModelViewer.error', 'Error: {msg}').replace('{msg}', String(msg)));
|
|
447
451
|
onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(msg));
|
|
448
452
|
}
|
|
449
453
|
finally {
|
|
450
454
|
setLoading(false);
|
|
451
455
|
setLoadProgress(1);
|
|
452
456
|
}
|
|
453
|
-
}, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount]);
|
|
457
|
+
}, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount, t]);
|
|
454
458
|
/* ── auto-load from src ── */
|
|
455
459
|
useEffect(() => {
|
|
456
460
|
if (src) {
|
|
@@ -591,9 +595,12 @@ function useModelViewer(props = {}) {
|
|
|
591
595
|
/* ── finalise status text ── */
|
|
592
596
|
useEffect(() => {
|
|
593
597
|
if (!loading && !error && polyCount > 0) {
|
|
594
|
-
setStatusText(
|
|
598
|
+
setStatusText(t('useModelViewer.trianglesMeshesAnimationS', '{polyCount} triangles · {meshCount} meshes · {animNames} animation(s)')
|
|
599
|
+
.replace('{polyCount}', String(polyCount.toLocaleString()))
|
|
600
|
+
.replace('{meshCount}', String(meshCount))
|
|
601
|
+
.replace('{animNames}', String(animNames.length)));
|
|
595
602
|
}
|
|
596
|
-
}, [loading, error, polyCount, meshCount, animNames.length]);
|
|
603
|
+
}, [loading, error, polyCount, meshCount, animNames.length, t]);
|
|
597
604
|
return {
|
|
598
605
|
mountRef,
|
|
599
606
|
loading,
|