@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/cjs/core/editorAsk.js +48 -0
  3. package/dist/cjs/core/i18n.js +7 -3
  4. package/dist/cjs/dance/DanceScoreEngine.js +7 -0
  5. package/dist/cjs/dance/PoseDetector.js +4 -2
  6. package/dist/cjs/material/NiceMaterialEditor.js +8 -3
  7. package/dist/cjs/material/materialEditorTypes.js +1 -1
  8. package/dist/cjs/material/materialNodeDefinitions.js +14 -11
  9. package/dist/cjs/model/ModelEditor.js +9 -2
  10. package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
  11. package/dist/cjs/model/ModelEditorMenuBar.js +10 -8
  12. package/dist/cjs/model/ModelEditorRightPanel.js +28 -22
  13. package/dist/cjs/model/ModelEditorSubComponents.js +17 -9
  14. package/dist/cjs/model/ModelEditorTimeline.js +8 -4
  15. package/dist/cjs/model/ModelEditorToolbar.js +6 -4
  16. package/dist/cjs/model/ModelEditorViewport.js +19 -6
  17. package/dist/cjs/model/ModelViewer.js +9 -7
  18. package/dist/cjs/model/NiceArmatureEditor.js +9 -7
  19. package/dist/cjs/model/NiceMorphTargetEditor.js +7 -3
  20. package/dist/cjs/model/NiceOctree.js +9 -4
  21. package/dist/cjs/model/NicePhysicsSimulation.js +7 -5
  22. package/dist/cjs/model/NiceProceduralGeometry.js +6 -4
  23. package/dist/cjs/model/NiceTerrainEditor.js +9 -7
  24. package/dist/cjs/model/NiceWeightPainter.js +6 -4
  25. package/dist/cjs/model/NiceXRPreview.js +9 -4
  26. package/dist/cjs/model/useModelEditor.js +113 -87
  27. package/dist/cjs/model/useModelViewer.js +13 -6
  28. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  29. package/dist/cjs/particle/NiceParticleEditor.js +23 -11
  30. package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
  31. package/dist/cjs/rendering/NiceRenderExport.js +9 -3
  32. package/dist/cjs/rendering/NiceSSAO.js +4 -1
  33. package/dist/cjs/rendering/NiceSSR.js +4 -1
  34. package/dist/cjs/rendering/NiceWebGPURenderer.js +5 -2
  35. package/dist/cjs/uv/NiceUVEditor.js +16 -12
  36. package/dist/cjs/uv/uvEditorTypes.js +1 -0
  37. package/dist/esm/core/editorAsk.js +45 -0
  38. package/dist/esm/core/i18n.js +8 -4
  39. package/dist/esm/dance/DanceScoreEngine.js +7 -0
  40. package/dist/esm/dance/PoseDetector.js +4 -2
  41. package/dist/esm/material/NiceMaterialEditor.js +8 -3
  42. package/dist/esm/material/materialEditorTypes.js +1 -1
  43. package/dist/esm/material/materialNodeDefinitions.js +14 -11
  44. package/dist/esm/model/ModelEditor.js +9 -2
  45. package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
  46. package/dist/esm/model/ModelEditorMenuBar.js +10 -8
  47. package/dist/esm/model/ModelEditorRightPanel.js +28 -22
  48. package/dist/esm/model/ModelEditorSubComponents.js +17 -9
  49. package/dist/esm/model/ModelEditorTimeline.js +8 -4
  50. package/dist/esm/model/ModelEditorToolbar.js +6 -4
  51. package/dist/esm/model/ModelEditorViewport.js +19 -6
  52. package/dist/esm/model/ModelViewer.js +9 -7
  53. package/dist/esm/model/NiceArmatureEditor.js +9 -7
  54. package/dist/esm/model/NiceMorphTargetEditor.js +7 -3
  55. package/dist/esm/model/NiceOctree.js +9 -4
  56. package/dist/esm/model/NicePhysicsSimulation.js +7 -5
  57. package/dist/esm/model/NiceProceduralGeometry.js +6 -4
  58. package/dist/esm/model/NiceTerrainEditor.js +9 -7
  59. package/dist/esm/model/NiceWeightPainter.js +6 -4
  60. package/dist/esm/model/NiceXRPreview.js +9 -4
  61. package/dist/esm/model/useModelEditor.js +113 -87
  62. package/dist/esm/model/useModelViewer.js +13 -6
  63. package/dist/esm/nice2dev-ui-3d.css +1 -1
  64. package/dist/esm/particle/NiceParticleEditor.js +23 -11
  65. package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
  66. package/dist/esm/rendering/NiceRenderExport.js +9 -3
  67. package/dist/esm/rendering/NiceSSAO.js +4 -1
  68. package/dist/esm/rendering/NiceSSR.js +4 -1
  69. package/dist/esm/rendering/NiceWebGPURenderer.js +5 -2
  70. package/dist/esm/uv/NiceUVEditor.js +16 -12
  71. package/dist/esm/uv/uvEditorTypes.js +1 -0
  72. package/dist/types/core/editorAsk.d.ts +26 -0
  73. package/dist/types/core/i18n.d.ts +1 -17
  74. package/dist/types/dance/DanceScoreEngine.d.ts +0 -6
  75. package/dist/types/model/useModelEditor.d.ts +3 -0
  76. 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, info: console.info };
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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(`Loaded: ${filename} (${clips.length} animation(s))`);
397
- }, [rebuildTree]);
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(`Loading ${file.name}...`);
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(`Unsupported format: ${ext}`);
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(`Error loading ${file.name}: ${String(err)}`);
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(`Merging animations from ${file.name}...`);
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(`No animations found in ${file.name}`);
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(`Merged ${clips.length} animation(s) from ${file.name}`);
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(`Error merging ${file.name}: ${String(err)}`);
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(`Duplicated ${selectedNode.name}`);
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(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
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(`Export error: ${String(err)}`);
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 OBJ successfully');
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(`Exported STL${binary ? ' (binary)' : ''} successfully`);
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(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
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 USDZ successfully');
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(`USDZ export error: ${String(err)}`);
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 clearScene = useCallback(() => {
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 defaultName = 'model.glb';
1283
- const name = window.prompt('Save to library as:', defaultName);
1284
- if (!name || !name.trim()) {
1285
- return;
1286
- }
1287
- onSaveToLibrary(reader.result, name.trim(), 'model/gltf-binary');
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(`Save error: ${String(err)}`);
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(`Added ${mesh.name}`);
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(`Added ${light.name}`);
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(`Added ${obj.name}`);
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(`Got ${result.frame_count} frames at ${result.fps} fps. Creating animation clip...`);
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(`Error: ${String(err)}`);
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(`Undo: ${label}`);
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(`Redo: ${label}`);
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(`Importing ${fileArray.length} file(s)...`);
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(`Imported ${loaded}/${fileArray.length}: ${file.name}`);
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(`Batch import complete: ${loaded}/${fileArray.length} files loaded`);
1628
- }, [loadFile]);
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: `Apply ${preset.name} preset`,
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(`Applied ${preset.name} preset`);
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: `Generate LOD for ${mesh.name}`,
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(`Generated LOD for ${mesh.name} (4 levels)`);
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(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
1729
- }, [rebuildTree]);
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(`Environment: ${preset.name}`);
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(`Loaded – ${polyCount} tris, ${meshCount} meshes`);
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(`Error: ${msg}`);
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(`${polyCount.toLocaleString()} triangles · ${meshCount} meshes · ${animNames.length} animation(s)`);
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,