@nice2dev/ui-3d 1.2.1 → 1.3.0
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 +415 -0
- package/dist/cjs/material/NiceMaterialEditor.js +1 -0
- package/dist/cjs/model/ModelEditor.js +7 -1
- package/dist/cjs/model/ModelEditorLeftPanel.js +2 -2
- package/dist/cjs/model/ModelEditorMenuBar.js +5 -5
- package/dist/cjs/model/ModelEditorRightPanel.js +13 -9
- package/dist/cjs/model/ModelEditorSubComponents.js +10 -4
- package/dist/cjs/model/ModelEditorTimeline.js +4 -2
- package/dist/cjs/model/ModelEditorToolbar.js +1 -1
- package/dist/cjs/model/ModelEditorViewport.js +6 -3
- package/dist/cjs/model/ModelViewer.js +4 -4
- package/dist/cjs/model/NiceArmatureEditor.js +4 -4
- package/dist/cjs/model/NiceMorphTargetEditor.js +3 -1
- package/dist/cjs/model/NiceOctree.js +7 -4
- package/dist/cjs/model/NicePhysicsSimulation.js +4 -4
- package/dist/cjs/model/NiceProceduralGeometry.js +3 -3
- package/dist/cjs/model/NiceTerrainEditor.js +2 -2
- package/dist/cjs/model/NiceWeightPainter.js +2 -2
- package/dist/cjs/model/NiceXRPreview.js +6 -3
- package/dist/cjs/model/useModelEditor.js +131 -79
- package/dist/cjs/model/useModelViewer.js +19 -12
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +12 -5
- package/dist/cjs/rendering/NiceCascadedShadows.js +1 -1
- package/dist/cjs/rendering/NiceRenderExport.js +2 -2
- package/dist/cjs/rendering/NiceSSAO.js +2 -1
- package/dist/cjs/rendering/NiceSSR.js +2 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +2 -1
- package/dist/cjs/uv/NiceUVEditor.js +3 -3
- package/dist/esm/material/NiceMaterialEditor.js +1 -0
- package/dist/esm/model/ModelEditor.js +7 -1
- package/dist/esm/model/ModelEditorLeftPanel.js +3 -3
- package/dist/esm/model/ModelEditorMenuBar.js +6 -6
- package/dist/esm/model/ModelEditorRightPanel.js +13 -9
- package/dist/esm/model/ModelEditorSubComponents.js +11 -5
- package/dist/esm/model/ModelEditorTimeline.js +5 -3
- package/dist/esm/model/ModelEditorToolbar.js +1 -1
- package/dist/esm/model/ModelEditorViewport.js +7 -4
- package/dist/esm/model/ModelViewer.js +4 -4
- package/dist/esm/model/NiceArmatureEditor.js +4 -4
- package/dist/esm/model/NiceMorphTargetEditor.js +3 -1
- package/dist/esm/model/NiceOctree.js +7 -4
- package/dist/esm/model/NicePhysicsSimulation.js +4 -4
- package/dist/esm/model/NiceProceduralGeometry.js +3 -3
- package/dist/esm/model/NiceTerrainEditor.js +2 -2
- package/dist/esm/model/NiceWeightPainter.js +2 -2
- package/dist/esm/model/NiceXRPreview.js +6 -3
- package/dist/esm/model/useModelEditor.js +131 -79
- package/dist/esm/model/useModelViewer.js +20 -13
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +13 -6
- package/dist/esm/rendering/NiceCascadedShadows.js +1 -1
- package/dist/esm/rendering/NiceRenderExport.js +2 -2
- package/dist/esm/rendering/NiceSSAO.js +2 -1
- package/dist/esm/rendering/NiceSSR.js +2 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +2 -1
- package/dist/esm/uv/NiceUVEditor.js +3 -3
- package/package.json +3 -3
|
@@ -22,6 +22,7 @@ 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
24
|
import { useEditorAsk } from '../core/editorAsk.js';
|
|
25
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
25
26
|
import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
|
|
26
27
|
import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
|
|
27
28
|
import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
|
|
@@ -55,6 +56,7 @@ function findNodeById(nodes, id) {
|
|
|
55
56
|
═══════════════════════════════════════════ */
|
|
56
57
|
function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
57
58
|
var _a;
|
|
59
|
+
const { t } = useNiceTranslation();
|
|
58
60
|
const { pendingAsk, ask } = useEditorAsk();
|
|
59
61
|
/* ── refs ── */
|
|
60
62
|
const mountRef = useRef(null);
|
|
@@ -394,14 +396,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
394
396
|
}
|
|
395
397
|
focusOnObjectRef.current(obj);
|
|
396
398
|
rebuildTree();
|
|
397
|
-
setStatusText(
|
|
398
|
-
|
|
399
|
+
setStatusText(t('useModelEditor.loadedAnimationS', 'Loaded: {filename} ({clips} animation(s))')
|
|
400
|
+
.replace('{filename}', String(filename))
|
|
401
|
+
.replace('{clips}', String(clips.length)));
|
|
402
|
+
}, [rebuildTree, t]);
|
|
399
403
|
const loadFile = useCallback(async (file) => {
|
|
400
404
|
var _a, _b, _c, _d, _e, _f;
|
|
401
405
|
const name = file.name.toLowerCase();
|
|
402
406
|
const ext = '.' + name.split('.').pop();
|
|
403
407
|
const url = URL.createObjectURL(file);
|
|
404
|
-
setStatusText(
|
|
408
|
+
setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
|
|
405
409
|
try {
|
|
406
410
|
if (ext === '.fbx') {
|
|
407
411
|
const loader = new FBXLoader();
|
|
@@ -525,32 +529,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
525
529
|
addObjectToScene(group, [], file.name);
|
|
526
530
|
}
|
|
527
531
|
else {
|
|
528
|
-
setStatusText(
|
|
532
|
+
setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
|
|
529
533
|
}
|
|
530
534
|
}
|
|
531
535
|
catch (err) {
|
|
532
536
|
log.error('Load error:', err);
|
|
533
|
-
setStatusText(
|
|
537
|
+
setStatusText(t('useModelEditor.errorLoading', 'Error loading {name}: {err}')
|
|
538
|
+
.replace('{name}', String(file.name))
|
|
539
|
+
.replace('{err}', String(String(err))));
|
|
534
540
|
}
|
|
535
541
|
finally {
|
|
536
542
|
URL.revokeObjectURL(url);
|
|
537
543
|
}
|
|
538
|
-
}, [addObjectToScene]);
|
|
544
|
+
}, [addObjectToScene, t]);
|
|
539
545
|
const mergeAnimationFBX = useCallback(async (file) => {
|
|
540
546
|
var _a;
|
|
541
547
|
const url = URL.createObjectURL(file);
|
|
542
|
-
setStatusText(
|
|
548
|
+
setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
|
|
543
549
|
try {
|
|
544
550
|
const loader = new FBXLoader();
|
|
545
551
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
546
552
|
const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
|
|
547
553
|
if (clips.length === 0) {
|
|
548
|
-
setStatusText(
|
|
554
|
+
setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
|
|
549
555
|
return;
|
|
550
556
|
}
|
|
551
557
|
const root = rootObjectRef.current;
|
|
552
558
|
if (!root) {
|
|
553
|
-
setStatusText('No model loaded — load a base model first');
|
|
559
|
+
setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
|
|
554
560
|
return;
|
|
555
561
|
}
|
|
556
562
|
if (!mixerRef.current) {
|
|
@@ -564,16 +570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
564
570
|
}));
|
|
565
571
|
allClipsRef.current = [...allClipsRef.current, ...newAnims];
|
|
566
572
|
setAnimations([...allClipsRef.current]);
|
|
567
|
-
setStatusText(
|
|
573
|
+
setStatusText(t('useModelEditor.mergedAnimationSFrom', 'Merged {clips} animation(s) from {name}')
|
|
574
|
+
.replace('{clips}', String(clips.length))
|
|
575
|
+
.replace('{name}', String(file.name)));
|
|
568
576
|
}
|
|
569
577
|
catch (err) {
|
|
570
578
|
log.error('Merge error:', err);
|
|
571
|
-
setStatusText(
|
|
579
|
+
setStatusText(t('useModelEditor.errorMerging', 'Error merging {name}: {err}')
|
|
580
|
+
.replace('{name}', String(file.name))
|
|
581
|
+
.replace('{err}', String(String(err))));
|
|
572
582
|
}
|
|
573
583
|
finally {
|
|
574
584
|
URL.revokeObjectURL(url);
|
|
575
585
|
}
|
|
576
|
-
}, []);
|
|
586
|
+
}, [t]);
|
|
577
587
|
/* ─────────────────────────────────────────
|
|
578
588
|
Animation controls
|
|
579
589
|
───────────────────────────────────────── */
|
|
@@ -931,18 +941,40 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
931
941
|
clone.position.x += 1;
|
|
932
942
|
sceneRef.current.add(clone);
|
|
933
943
|
rebuildTree();
|
|
934
|
-
setStatusText(
|
|
935
|
-
}, [selectedNode, rebuildTree]);
|
|
944
|
+
setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
|
|
945
|
+
}, [selectedNode, rebuildTree, t]);
|
|
936
946
|
/* ─────────────────────────────────────────
|
|
937
947
|
Keyboard shortcuts
|
|
938
948
|
───────────────────────────────────────── */
|
|
939
949
|
useEffect(() => {
|
|
950
|
+
// Escape, then Tab, leaves the viewport, where Tab cycles the mode (WCAG 2.1.2)
|
|
951
|
+
let tabEscape = false;
|
|
940
952
|
const handleKey = (e) => {
|
|
941
|
-
var _a, _b, _c;
|
|
942
|
-
|
|
943
|
-
|
|
953
|
+
var _a, _b, _c, _d;
|
|
954
|
+
// a key sent to the window itself counts as the body's
|
|
955
|
+
const target = e.target instanceof HTMLElement ? e.target : null;
|
|
956
|
+
const tag = target === null || target === void 0 ? void 0 : target.tagName;
|
|
957
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (target === null || target === void 0 ? void 0 : target.isContentEditable)) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
// The shortcuts are this editor's: keys elsewhere on the page are not, and Tab / Space on
|
|
961
|
+
// one of its controls move focus and press it. They listened on the whole page — Tab moved
|
|
962
|
+
// focus nowhere on a page with the editor and Space pressed no button (TODO_1.3.0 I4).
|
|
963
|
+
const root = (_b = (_a = mountRef.current) === null || _a === void 0 ? void 0 : _a.closest('.nice-app-shell')) !== null && _b !== void 0 ? _b : mountRef.current;
|
|
964
|
+
if (target && target !== document.body && !(root === null || root === void 0 ? void 0 : root.contains(target))) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if ((e.key === 'Tab' || e.key === ' ') &&
|
|
968
|
+
(target === null || target === void 0 ? void 0 : target.closest('button, a[href], [role="button"], [role="combobox"], [role="menuitem"], [role="tab"], [role="option"], [role="checkbox"], [role="switch"], [role="slider"], [role="treeitem"]'))) {
|
|
944
969
|
return;
|
|
945
970
|
}
|
|
971
|
+
if (e.key === 'Tab' && tabEscape) {
|
|
972
|
+
tabEscape = false;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (!['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) {
|
|
976
|
+
tabEscape = e.key === 'Escape';
|
|
977
|
+
}
|
|
946
978
|
switch (e.key.toLowerCase()) {
|
|
947
979
|
case 'g':
|
|
948
980
|
setTransformMode('translate');
|
|
@@ -969,7 +1001,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
969
1001
|
const sel = findNodeById(sceneTree, selectedId);
|
|
970
1002
|
if (sel) {
|
|
971
1003
|
sel.object.removeFromParent();
|
|
972
|
-
(
|
|
1004
|
+
(_c = transformRef.current) === null || _c === void 0 ? void 0 : _c.detach();
|
|
973
1005
|
setSelectedId(null);
|
|
974
1006
|
rebuildTree();
|
|
975
1007
|
}
|
|
@@ -980,7 +1012,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
980
1012
|
const sel = findNodeById(sceneTree, selectedId);
|
|
981
1013
|
if (sel) {
|
|
982
1014
|
sel.object.removeFromParent();
|
|
983
|
-
(
|
|
1015
|
+
(_d = transformRef.current) === null || _d === void 0 ? void 0 : _d.detach();
|
|
984
1016
|
setSelectedId(null);
|
|
985
1017
|
rebuildTree();
|
|
986
1018
|
}
|
|
@@ -1076,7 +1108,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1076
1108
|
const exportGLTF = useCallback((binary) => {
|
|
1077
1109
|
const root = rootObjectRef.current;
|
|
1078
1110
|
if (!root) {
|
|
1079
|
-
setStatusText('Nothing to export');
|
|
1111
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1080
1112
|
return;
|
|
1081
1113
|
}
|
|
1082
1114
|
const exporter = new GLTFExporter();
|
|
@@ -1097,16 +1129,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1097
1129
|
a.download = `export.${binary ? 'glb' : 'gltf'}`;
|
|
1098
1130
|
a.click();
|
|
1099
1131
|
URL.revokeObjectURL(a.href);
|
|
1100
|
-
setStatusText(
|
|
1132
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? 'GLB' : 'GLTF'));
|
|
1101
1133
|
}, (err) => {
|
|
1102
1134
|
log.error('Export error:', err);
|
|
1103
|
-
setStatusText(
|
|
1135
|
+
setStatusText(t('useModelEditor.exportError', 'Export error: {err}').replace('{err}', String(String(err))));
|
|
1104
1136
|
}, options);
|
|
1105
|
-
}, []);
|
|
1137
|
+
}, [t]);
|
|
1106
1138
|
const exportOBJ = useCallback(() => {
|
|
1107
1139
|
const root = rootObjectRef.current;
|
|
1108
1140
|
if (!root) {
|
|
1109
|
-
setStatusText('Nothing to export');
|
|
1141
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1110
1142
|
return;
|
|
1111
1143
|
}
|
|
1112
1144
|
const exporter = new OBJExporter();
|
|
@@ -1117,12 +1149,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1117
1149
|
a.download = 'export.obj';
|
|
1118
1150
|
a.click();
|
|
1119
1151
|
URL.revokeObjectURL(a.href);
|
|
1120
|
-
setStatusText('Exported
|
|
1121
|
-
}, []);
|
|
1152
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'OBJ'));
|
|
1153
|
+
}, [t]);
|
|
1122
1154
|
const exportSTL = useCallback((binary) => {
|
|
1123
1155
|
const root = rootObjectRef.current;
|
|
1124
1156
|
if (!root) {
|
|
1125
|
-
setStatusText('Nothing to export');
|
|
1157
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1126
1158
|
return;
|
|
1127
1159
|
}
|
|
1128
1160
|
const exporter = new STLExporter();
|
|
@@ -1139,12 +1171,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1139
1171
|
a.download = 'export.stl';
|
|
1140
1172
|
a.click();
|
|
1141
1173
|
URL.revokeObjectURL(a.href);
|
|
1142
|
-
setStatusText(
|
|
1143
|
-
}, []);
|
|
1174
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.stlBinary', 'STL (binary)') : 'STL'));
|
|
1175
|
+
}, [t]);
|
|
1144
1176
|
const exportPLY = useCallback((binary) => {
|
|
1145
1177
|
const root = rootObjectRef.current;
|
|
1146
1178
|
if (!root) {
|
|
1147
|
-
setStatusText('Nothing to export');
|
|
1179
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1148
1180
|
return;
|
|
1149
1181
|
}
|
|
1150
1182
|
const exporter = new PLYExporter();
|
|
@@ -1162,13 +1194,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1162
1194
|
a.download = 'export.ply';
|
|
1163
1195
|
a.click();
|
|
1164
1196
|
URL.revokeObjectURL(a.href);
|
|
1165
|
-
setStatusText(
|
|
1197
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.plyBinary', 'PLY (binary)') : 'PLY'));
|
|
1166
1198
|
}, { binary });
|
|
1167
|
-
}, []);
|
|
1199
|
+
}, [t]);
|
|
1168
1200
|
const exportUSDZ = useCallback(async () => {
|
|
1169
1201
|
const root = rootObjectRef.current;
|
|
1170
1202
|
if (!root) {
|
|
1171
|
-
setStatusText('Nothing to export');
|
|
1203
|
+
setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
|
|
1172
1204
|
return;
|
|
1173
1205
|
}
|
|
1174
1206
|
try {
|
|
@@ -1183,13 +1215,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1183
1215
|
a.download = 'export.usdz';
|
|
1184
1216
|
a.click();
|
|
1185
1217
|
URL.revokeObjectURL(a.href);
|
|
1186
|
-
setStatusText('Exported
|
|
1218
|
+
setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'USDZ'));
|
|
1187
1219
|
}
|
|
1188
1220
|
catch (err) {
|
|
1189
1221
|
log.error('USDZ export error:', err);
|
|
1190
|
-
setStatusText(
|
|
1222
|
+
setStatusText(t('useModelEditor.usdzExportError', 'USDZ export error: {err}').replace('{err}', String(String(err))));
|
|
1191
1223
|
}
|
|
1192
|
-
}, []);
|
|
1224
|
+
}, [t]);
|
|
1193
1225
|
/* ─────────────────────────────────────────
|
|
1194
1226
|
Dispose & clear
|
|
1195
1227
|
───────────────────────────────────────── */
|
|
@@ -1258,8 +1290,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1258
1290
|
setAnimDuration(0);
|
|
1259
1291
|
setSelectedId(null);
|
|
1260
1292
|
rebuildTree();
|
|
1261
|
-
setStatusText('Scene cleared');
|
|
1262
|
-
}, [rebuildTree, disposeObject]);
|
|
1293
|
+
setStatusText(t('useModelEditor.sceneCleared', 'Scene cleared'));
|
|
1294
|
+
}, [rebuildTree, disposeObject, t]);
|
|
1263
1295
|
// asks first: clearing cannot be undone
|
|
1264
1296
|
const clearScene = useCallback(() => {
|
|
1265
1297
|
void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
|
|
@@ -1277,7 +1309,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1277
1309
|
}
|
|
1278
1310
|
const root = rootObjectRef.current;
|
|
1279
1311
|
if (!root) {
|
|
1280
|
-
setStatusText('Nothing to save');
|
|
1312
|
+
setStatusText(t('useModelEditor.nothingToSave', 'Nothing to save'));
|
|
1281
1313
|
return;
|
|
1282
1314
|
}
|
|
1283
1315
|
const exporter = new GLTFExporter();
|
|
@@ -1295,9 +1327,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1295
1327
|
reader.readAsDataURL(blob);
|
|
1296
1328
|
}, (err) => {
|
|
1297
1329
|
log.error('Save to library error:', err);
|
|
1298
|
-
setStatusText(
|
|
1330
|
+
setStatusText(t('useModelEditor.saveError', 'Save error: {err}').replace('{err}', String(String(err))));
|
|
1299
1331
|
}, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
|
|
1300
|
-
}, [onSaveToLibrary, ask]);
|
|
1332
|
+
}, [onSaveToLibrary, ask, t]);
|
|
1301
1333
|
/* ─────────────────────────────────────────
|
|
1302
1334
|
Add Mesh / Light / Camera / Empty
|
|
1303
1335
|
───────────────────────────────────────── */
|
|
@@ -1364,9 +1396,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1364
1396
|
}
|
|
1365
1397
|
sceneRef.current.add(mesh);
|
|
1366
1398
|
rebuildTree();
|
|
1367
|
-
setStatusText(
|
|
1399
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(mesh.name)));
|
|
1368
1400
|
setAddMenuOpen(null);
|
|
1369
|
-
}, [rebuildTree]);
|
|
1401
|
+
}, [rebuildTree, t]);
|
|
1370
1402
|
const addSceneLight = useCallback((type) => {
|
|
1371
1403
|
let light;
|
|
1372
1404
|
switch (type) {
|
|
@@ -1418,9 +1450,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1418
1450
|
sceneRef.current.add(helper);
|
|
1419
1451
|
}
|
|
1420
1452
|
rebuildTree();
|
|
1421
|
-
setStatusText(
|
|
1453
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(light.name)));
|
|
1422
1454
|
setAddMenuOpen(null);
|
|
1423
|
-
}, [rebuildTree]);
|
|
1455
|
+
}, [rebuildTree, t]);
|
|
1424
1456
|
const addCameraObject = useCallback(() => {
|
|
1425
1457
|
const cam = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
|
|
1426
1458
|
cam.name = 'Camera';
|
|
@@ -1431,9 +1463,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1431
1463
|
helper.name = '__helper_Camera';
|
|
1432
1464
|
sceneRef.current.add(helper);
|
|
1433
1465
|
rebuildTree();
|
|
1434
|
-
setStatusText('Added Camera');
|
|
1466
|
+
setStatusText(t('useModelEditor.addedCamera', 'Added Camera'));
|
|
1435
1467
|
setAddMenuOpen(null);
|
|
1436
|
-
}, [rebuildTree]);
|
|
1468
|
+
}, [rebuildTree, t]);
|
|
1437
1469
|
const addEmpty = useCallback((type) => {
|
|
1438
1470
|
const obj = new THREE.Object3D();
|
|
1439
1471
|
obj.name =
|
|
@@ -1445,9 +1477,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1445
1477
|
obj.add(h);
|
|
1446
1478
|
}
|
|
1447
1479
|
rebuildTree();
|
|
1448
|
-
setStatusText(
|
|
1480
|
+
setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(obj.name)));
|
|
1449
1481
|
setAddMenuOpen(null);
|
|
1450
|
-
}, [rebuildTree]);
|
|
1482
|
+
}, [rebuildTree, t]);
|
|
1451
1483
|
/* ─────────────────────────────────────────
|
|
1452
1484
|
Raycaster click-to-select
|
|
1453
1485
|
───────────────────────────────────────── */
|
|
@@ -1515,14 +1547,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1515
1547
|
}
|
|
1516
1548
|
e.target.value = '';
|
|
1517
1549
|
if (!onAIPoseVideo) {
|
|
1518
|
-
setAiStatus('AI pose callback not configured.');
|
|
1550
|
+
setAiStatus(t('useModelEditor.aiPoseCallbackNotConfigured', 'AI pose callback not configured.'));
|
|
1519
1551
|
return;
|
|
1520
1552
|
}
|
|
1521
1553
|
setAiBusy(true);
|
|
1522
|
-
setAiStatus('Uploading video for AI pose estimation...');
|
|
1554
|
+
setAiStatus(t('useModelEditor.uploadingVideoForAiPose', 'Uploading video for AI pose estimation...'));
|
|
1523
1555
|
try {
|
|
1524
1556
|
const result = await onAIPoseVideo(file);
|
|
1525
|
-
setAiStatus(
|
|
1557
|
+
setAiStatus(t('useModelEditor.gotFramesAtFpsCreating', 'Got {frame_count} frames at {fps} fps. Creating animation clip...')
|
|
1558
|
+
.replace('{frame_count}', String(result.frame_count))
|
|
1559
|
+
.replace('{fps}', String(result.fps)));
|
|
1526
1560
|
const clip = pose3dToClip(result);
|
|
1527
1561
|
if (clip && rootObjectRef.current) {
|
|
1528
1562
|
if (!mixerRef.current) {
|
|
@@ -1537,20 +1571,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1537
1571
|
allClipsRef.current = [...allClipsRef.current, newAnim];
|
|
1538
1572
|
setAnimations([...allClipsRef.current]);
|
|
1539
1573
|
playClip(allClipsRef.current.length - 1);
|
|
1540
|
-
setAiStatus('Animation created successfully!');
|
|
1574
|
+
setAiStatus(t('useModelEditor.animationCreatedSuccessfully', 'Animation created successfully!'));
|
|
1541
1575
|
}
|
|
1542
1576
|
else {
|
|
1543
|
-
setAiStatus('Could not create animation — load a rigged model first.');
|
|
1577
|
+
setAiStatus(t('useModelEditor.couldNotCreateAnimationLoad', 'Could not create animation — load a rigged model first.'));
|
|
1544
1578
|
}
|
|
1545
1579
|
}
|
|
1546
1580
|
catch (err) {
|
|
1547
1581
|
log.error('AI pose error:', err);
|
|
1548
|
-
setAiStatus(
|
|
1582
|
+
setAiStatus(t('useModelEditor.error', 'Error: {err}').replace('{err}', String(String(err))));
|
|
1549
1583
|
}
|
|
1550
1584
|
finally {
|
|
1551
1585
|
setAiBusy(false);
|
|
1552
1586
|
}
|
|
1553
|
-
}, [playClip, onAIPoseVideo]);
|
|
1587
|
+
}, [playClip, onAIPoseVideo, t]);
|
|
1554
1588
|
/* ─────────────────────────────────────────
|
|
1555
1589
|
Node helpers
|
|
1556
1590
|
───────────────────────────────────────── */
|
|
@@ -1574,26 +1608,37 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1574
1608
|
const undo = useCallback(() => {
|
|
1575
1609
|
const label = undoRedoRef.current.undo();
|
|
1576
1610
|
if (label) {
|
|
1577
|
-
setStatusText(
|
|
1611
|
+
setStatusText(t('useModelEditor.undo', 'Undo: {label}').replace('{label}', String(label)));
|
|
1578
1612
|
rebuildTree();
|
|
1579
1613
|
}
|
|
1580
1614
|
setUndoRedoVer((v) => v + 1);
|
|
1581
|
-
}, [rebuildTree]);
|
|
1615
|
+
}, [rebuildTree, t]);
|
|
1582
1616
|
const redo = useCallback(() => {
|
|
1583
1617
|
const label = undoRedoRef.current.redo();
|
|
1584
1618
|
if (label) {
|
|
1585
|
-
setStatusText(
|
|
1619
|
+
setStatusText(t('useModelEditor.redo', 'Redo: {label}').replace('{label}', String(label)));
|
|
1586
1620
|
rebuildTree();
|
|
1587
1621
|
}
|
|
1588
1622
|
setUndoRedoVer((v) => v + 1);
|
|
1589
|
-
}, [rebuildTree]);
|
|
1623
|
+
}, [rebuildTree, t]);
|
|
1590
1624
|
const canUndo = undoRedoRef.current.canUndo();
|
|
1591
1625
|
const canRedo = undoRedoRef.current.canRedo();
|
|
1592
1626
|
const undoLabel = undoRedoRef.current.undoLabel;
|
|
1593
1627
|
const redoLabel = undoRedoRef.current.redoLabel;
|
|
1594
|
-
// Keyboard shortcuts for undo/redo
|
|
1628
|
+
// Keyboard shortcuts for undo/redo — in this editor only, and not in a text field, whose own
|
|
1629
|
+
// undo they took (on the whole page before)
|
|
1595
1630
|
useEffect(() => {
|
|
1596
1631
|
const handleKeyDown = (e) => {
|
|
1632
|
+
var _a, _b;
|
|
1633
|
+
const target = e.target instanceof HTMLElement ? e.target : null;
|
|
1634
|
+
const tag = target === null || target === void 0 ? void 0 : target.tagName;
|
|
1635
|
+
const root = (_b = (_a = mountRef.current) === null || _a === void 0 ? void 0 : _a.closest('.nice-app-shell')) !== null && _b !== void 0 ? _b : mountRef.current;
|
|
1636
|
+
if (tag === 'INPUT' ||
|
|
1637
|
+
tag === 'TEXTAREA' ||
|
|
1638
|
+
(target === null || target === void 0 ? void 0 : target.isContentEditable) ||
|
|
1639
|
+
(target && target !== document.body && !(root === null || root === void 0 ? void 0 : root.contains(target)))) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1597
1642
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
1598
1643
|
e.preventDefault();
|
|
1599
1644
|
undo();
|
|
@@ -1615,30 +1660,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1615
1660
|
return SUPPORTED_EXTENSIONS.includes(ext);
|
|
1616
1661
|
});
|
|
1617
1662
|
if (fileArray.length === 0) {
|
|
1618
|
-
setStatusText('No supported files found');
|
|
1663
|
+
setStatusText(t('useModelEditor.noSupportedFilesFound', 'No supported files found'));
|
|
1619
1664
|
return;
|
|
1620
1665
|
}
|
|
1621
|
-
setStatusText(
|
|
1666
|
+
setStatusText(t('useModelEditor.importingFileS', 'Importing {fileArray} file(s)...').replace('{fileArray}', String(fileArray.length)));
|
|
1622
1667
|
let loaded = 0;
|
|
1623
1668
|
for (const file of fileArray) {
|
|
1624
1669
|
try {
|
|
1625
1670
|
await loadFile(file);
|
|
1626
1671
|
loaded++;
|
|
1627
|
-
setStatusText(
|
|
1672
|
+
setStatusText(t('useModelEditor.imported', 'Imported {loaded}/{fileArray}: {name}')
|
|
1673
|
+
.replace('{loaded}', String(loaded))
|
|
1674
|
+
.replace('{fileArray}', String(fileArray.length))
|
|
1675
|
+
.replace('{name}', String(file.name)));
|
|
1628
1676
|
}
|
|
1629
1677
|
catch (err) {
|
|
1630
1678
|
log.error(`Failed to import ${file.name}:`, err);
|
|
1631
1679
|
}
|
|
1632
1680
|
}
|
|
1633
|
-
setStatusText(
|
|
1634
|
-
|
|
1681
|
+
setStatusText(t('useModelEditor.batchImportCompleteFilesLoaded', 'Batch import complete: {loaded}/{fileArray} files loaded')
|
|
1682
|
+
.replace('{loaded}', String(loaded))
|
|
1683
|
+
.replace('{fileArray}', String(fileArray.length)));
|
|
1684
|
+
}, [loadFile, t]);
|
|
1635
1685
|
/* ─────────────────────────────────────────
|
|
1636
1686
|
PBR Presets
|
|
1637
1687
|
───────────────────────────────────────── */
|
|
1638
1688
|
const applyPreset = useCallback((preset) => {
|
|
1639
1689
|
const node = selectedNode;
|
|
1640
1690
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1641
|
-
setStatusText('Select a mesh to apply material preset');
|
|
1691
|
+
setStatusText(t('useModelEditor.selectAMeshToApply', 'Select a mesh to apply material preset'));
|
|
1642
1692
|
return;
|
|
1643
1693
|
}
|
|
1644
1694
|
const mesh = node.object;
|
|
@@ -1671,7 +1721,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1671
1721
|
mesh.material = newMaterial;
|
|
1672
1722
|
}
|
|
1673
1723
|
pushUndo({
|
|
1674
|
-
label:
|
|
1724
|
+
label: t('useModelEditor.applyPreset', 'Apply {name} preset').replace('{name}', String(preset.name)),
|
|
1675
1725
|
undo: () => {
|
|
1676
1726
|
if (Array.isArray(mesh.material)) {
|
|
1677
1727
|
mesh.material[materialIdx] = oldClone;
|
|
@@ -1690,15 +1740,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1690
1740
|
},
|
|
1691
1741
|
});
|
|
1692
1742
|
setMatRefresh((v) => v + 1);
|
|
1693
|
-
setStatusText(
|
|
1694
|
-
}, [selectedNode, selMaterialIdx, pushUndo]);
|
|
1743
|
+
setStatusText(t('useModelEditor.appliedPreset', 'Applied {name} preset').replace('{name}', String(preset.name)));
|
|
1744
|
+
}, [selectedNode, selMaterialIdx, pushUndo, t]);
|
|
1695
1745
|
/* ─────────────────────────────────────────
|
|
1696
1746
|
LOD Generation
|
|
1697
1747
|
───────────────────────────────────────── */
|
|
1698
1748
|
const generateSelectedLOD = useCallback(() => {
|
|
1699
1749
|
const node = selectedNode;
|
|
1700
1750
|
if (!node || !(node.object instanceof THREE.Mesh)) {
|
|
1701
|
-
setStatusText('Select a mesh to generate LOD');
|
|
1751
|
+
setStatusText(t('useModelEditor.selectAMeshToGenerate', 'Select a mesh to generate LOD'));
|
|
1702
1752
|
return;
|
|
1703
1753
|
}
|
|
1704
1754
|
const mesh = node.object;
|
|
@@ -1707,7 +1757,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1707
1757
|
parent.remove(mesh);
|
|
1708
1758
|
parent.add(lod);
|
|
1709
1759
|
pushUndo({
|
|
1710
|
-
label:
|
|
1760
|
+
label: t('useModelEditor.generateLodFor', 'Generate LOD for {name}').replace('{name}', String(mesh.name)),
|
|
1711
1761
|
undo: () => {
|
|
1712
1762
|
parent.remove(lod);
|
|
1713
1763
|
parent.add(mesh);
|
|
@@ -1718,8 +1768,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1718
1768
|
},
|
|
1719
1769
|
});
|
|
1720
1770
|
rebuildTree();
|
|
1721
|
-
setStatusText(
|
|
1722
|
-
}, [selectedNode, pushUndo, rebuildTree]);
|
|
1771
|
+
setStatusText(t('useModelEditor.generatedLodFor4Levels', 'Generated LOD for {name} (4 levels)').replace('{name}', String(mesh.name)));
|
|
1772
|
+
}, [selectedNode, pushUndo, rebuildTree, t]);
|
|
1723
1773
|
/* ─────────────────────────────────────────
|
|
1724
1774
|
Instancing
|
|
1725
1775
|
───────────────────────────────────────── */
|
|
@@ -1727,12 +1777,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1727
1777
|
const root = rootObjectRef.current || sceneRef.current;
|
|
1728
1778
|
const result = convertToInstanced(root);
|
|
1729
1779
|
if (result.created === 0) {
|
|
1730
|
-
setStatusText('No duplicate meshes found to instance');
|
|
1780
|
+
setStatusText(t('useModelEditor.noDuplicateMeshesFoundTo', 'No duplicate meshes found to instance'));
|
|
1731
1781
|
return;
|
|
1732
1782
|
}
|
|
1733
1783
|
rebuildTree();
|
|
1734
|
-
setStatusText(
|
|
1735
|
-
|
|
1784
|
+
setStatusText(t('useModelEditor.createdInstancedMeshesRemovedDuplicates', 'Created {created} instanced meshes, removed {removed} duplicates')
|
|
1785
|
+
.replace('{created}', String(result.created))
|
|
1786
|
+
.replace('{removed}', String(result.removed)));
|
|
1787
|
+
}, [rebuildTree, t]);
|
|
1736
1788
|
/* ── HDRI Environment ── */
|
|
1737
1789
|
const applyHDRI = useCallback((preset) => {
|
|
1738
1790
|
const renderer = rendererRef.current;
|
|
@@ -1750,8 +1802,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1750
1802
|
else {
|
|
1751
1803
|
setFogEnabled(false);
|
|
1752
1804
|
}
|
|
1753
|
-
setStatusText(
|
|
1754
|
-
}, []);
|
|
1805
|
+
setStatusText(t('useModelEditor.environment', 'Environment: {name}').replace('{name}', String(preset.name)));
|
|
1806
|
+
}, [t]);
|
|
1755
1807
|
/* ── Snap ── */
|
|
1756
1808
|
const snapPoint = useCallback((point) => {
|
|
1757
1809
|
const root = rootObjectRef.current || sceneRef.current;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveCssColor } from '@nice2dev/ui';
|
|
1
|
+
import { resolveCssColor, getInputEngine } from '@nice2dev/ui';
|
|
2
2
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
@@ -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);
|
|
@@ -160,7 +162,7 @@ function useModelViewer(props = {}) {
|
|
|
160
162
|
ro.observe(mount);
|
|
161
163
|
// Animation loop
|
|
162
164
|
const animate = () => {
|
|
163
|
-
var _a, _b, _c, _d, _e, _f
|
|
165
|
+
var _a, _b, _c, _d, _e, _f;
|
|
164
166
|
if (disposeRef.current) {
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
@@ -168,12 +170,12 @@ function useModelViewer(props = {}) {
|
|
|
168
170
|
const dt = (_b = (_a = clockRef.current) === null || _a === void 0 ? void 0 : _a.getDelta()) !== null && _b !== void 0 ? _b : 0;
|
|
169
171
|
// Gamepad FPS-style nav
|
|
170
172
|
if (gamepad && navMode === 'fps') {
|
|
171
|
-
|
|
172
|
-
const gp = (
|
|
173
|
+
// The pads the input engine read this frame (1.3.0: the engine is the only pad reader).
|
|
174
|
+
const gp = (_c = getInputEngine().gamepadSnapshot[0]) !== null && _c !== void 0 ? _c : null;
|
|
173
175
|
if (gp) {
|
|
174
|
-
const lx = (
|
|
175
|
-
const ly = (
|
|
176
|
-
const rx = (
|
|
176
|
+
const lx = (_d = gp.axes[0]) !== null && _d !== void 0 ? _d : 0;
|
|
177
|
+
const ly = (_e = gp.axes[1]) !== null && _e !== void 0 ? _e : 0;
|
|
178
|
+
const rx = (_f = gp.axes[2]) !== null && _f !== void 0 ? _f : 0;
|
|
177
179
|
const fwd = new THREE.Vector3();
|
|
178
180
|
camera.getWorldDirection(fwd);
|
|
179
181
|
fwd.y = 0;
|
|
@@ -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,
|