@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
@@ -23,6 +23,8 @@ var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
23
23
  var SVGLoader_js = require('three/examples/jsm/loaders/SVGLoader.js');
24
24
  var TDSLoader_js = require('three/examples/jsm/loaders/TDSLoader.js');
25
25
  var VTKLoader_js = require('three/examples/jsm/loaders/VTKLoader.js');
26
+ var editorAsk = require('../core/editorAsk.js');
27
+ var i18n = require('../core/i18n.js');
26
28
  var threeCompat = require('../core/threeCompat.js');
27
29
  var modelEditorTypes = require('./modelEditorTypes.js');
28
30
  var modelEditorUtils = require('./modelEditorUtils.js');
@@ -53,7 +55,7 @@ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
53
55
  *
54
56
  * Extracted from the original ModelEditor.tsx god-component.
55
57
  */
56
- const log = { error: console.error, warn: console.warn, info: console.info };
58
+ const log = { error: console.error, warn: console.warn };
57
59
  /* helpers */
58
60
  let _nid = 0;
59
61
  const nid = () => `n${++_nid}`;
@@ -75,6 +77,8 @@ function findNodeById(nodes, id) {
75
77
  ═══════════════════════════════════════════ */
76
78
  function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
77
79
  var _a;
80
+ const { t } = i18n.useNiceTranslation();
81
+ const { pendingAsk, ask } = editorAsk.useEditorAsk();
78
82
  /* ── refs ── */
79
83
  const mountRef = React.useRef(null);
80
84
  const rendererRef = React.useRef(null);
@@ -225,10 +229,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
225
229
  // goes into the scene is its helper.
226
230
  const tcHelper = tc.getHelper();
227
231
  tcHelper.name = '__transformControls';
228
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
- tc.addEventListener('dragging-changed', ((e) => {
232
+ tc.addEventListener('dragging-changed', (e) => {
230
233
  orbit.enabled = !e.value;
231
- }));
234
+ });
232
235
  scene.add(tcHelper);
233
236
  transformRef.current = tc;
234
237
  // Resize handler
@@ -414,14 +417,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
414
417
  }
415
418
  focusOnObjectRef.current(obj);
416
419
  rebuildTree();
417
- setStatusText(`Loaded: ${filename} (${clips.length} animation(s))`);
418
- }, [rebuildTree]);
420
+ setStatusText(t('useModelEditor.loadedAnimationS', 'Loaded: {filename} ({clips} animation(s))')
421
+ .replace('{filename}', String(filename))
422
+ .replace('{clips}', String(clips.length)));
423
+ }, [rebuildTree, t]);
419
424
  const loadFile = React.useCallback(async (file) => {
420
425
  var _a, _b, _c, _d, _e, _f;
421
426
  const name = file.name.toLowerCase();
422
427
  const ext = '.' + name.split('.').pop();
423
428
  const url = URL.createObjectURL(file);
424
- setStatusText(`Loading ${file.name}...`);
429
+ setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
425
430
  try {
426
431
  if (ext === '.fbx') {
427
432
  const loader = new FBXLoader_js.FBXLoader();
@@ -545,32 +550,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
545
550
  addObjectToScene(group, [], file.name);
546
551
  }
547
552
  else {
548
- setStatusText(`Unsupported format: ${ext}`);
553
+ setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
549
554
  }
550
555
  }
551
556
  catch (err) {
552
557
  log.error('Load error:', err);
553
- setStatusText(`Error loading ${file.name}: ${String(err)}`);
558
+ setStatusText(t('useModelEditor.errorLoading', 'Error loading {name}: {err}')
559
+ .replace('{name}', String(file.name))
560
+ .replace('{err}', String(String(err))));
554
561
  }
555
562
  finally {
556
563
  URL.revokeObjectURL(url);
557
564
  }
558
- }, [addObjectToScene]);
565
+ }, [addObjectToScene, t]);
559
566
  const mergeAnimationFBX = React.useCallback(async (file) => {
560
567
  var _a;
561
568
  const url = URL.createObjectURL(file);
562
- setStatusText(`Merging animations from ${file.name}...`);
569
+ setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
563
570
  try {
564
571
  const loader = new FBXLoader_js.FBXLoader();
565
572
  const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
566
573
  const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
567
574
  if (clips.length === 0) {
568
- setStatusText(`No animations found in ${file.name}`);
575
+ setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
569
576
  return;
570
577
  }
571
578
  const root = rootObjectRef.current;
572
579
  if (!root) {
573
- setStatusText('No model loaded — load a base model first');
580
+ setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
574
581
  return;
575
582
  }
576
583
  if (!mixerRef.current) {
@@ -584,16 +591,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
584
591
  }));
585
592
  allClipsRef.current = [...allClipsRef.current, ...newAnims];
586
593
  setAnimations([...allClipsRef.current]);
587
- setStatusText(`Merged ${clips.length} animation(s) from ${file.name}`);
594
+ setStatusText(t('useModelEditor.mergedAnimationSFrom', 'Merged {clips} animation(s) from {name}')
595
+ .replace('{clips}', String(clips.length))
596
+ .replace('{name}', String(file.name)));
588
597
  }
589
598
  catch (err) {
590
599
  log.error('Merge error:', err);
591
- setStatusText(`Error merging ${file.name}: ${String(err)}`);
600
+ setStatusText(t('useModelEditor.errorMerging', 'Error merging {name}: {err}')
601
+ .replace('{name}', String(file.name))
602
+ .replace('{err}', String(String(err))));
592
603
  }
593
604
  finally {
594
605
  URL.revokeObjectURL(url);
595
606
  }
596
- }, []);
607
+ }, [t]);
597
608
  /* ─────────────────────────────────────────
598
609
  Animation controls
599
610
  ───────────────────────────────────────── */
@@ -951,8 +962,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
951
962
  clone.position.x += 1;
952
963
  sceneRef.current.add(clone);
953
964
  rebuildTree();
954
- setStatusText(`Duplicated ${selectedNode.name}`);
955
- }, [selectedNode, rebuildTree]);
965
+ setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
966
+ }, [selectedNode, rebuildTree, t]);
956
967
  /* ─────────────────────────────────────────
957
968
  Keyboard shortcuts
958
969
  ───────────────────────────────────────── */
@@ -1096,7 +1107,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1096
1107
  const exportGLTF = React.useCallback((binary) => {
1097
1108
  const root = rootObjectRef.current;
1098
1109
  if (!root) {
1099
- setStatusText('Nothing to export');
1110
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1100
1111
  return;
1101
1112
  }
1102
1113
  const exporter = new GLTFExporter_js.GLTFExporter();
@@ -1117,16 +1128,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1117
1128
  a.download = `export.${binary ? 'glb' : 'gltf'}`;
1118
1129
  a.click();
1119
1130
  URL.revokeObjectURL(a.href);
1120
- setStatusText(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
1131
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? 'GLB' : 'GLTF'));
1121
1132
  }, (err) => {
1122
1133
  log.error('Export error:', err);
1123
- setStatusText(`Export error: ${String(err)}`);
1134
+ setStatusText(t('useModelEditor.exportError', 'Export error: {err}').replace('{err}', String(String(err))));
1124
1135
  }, options);
1125
- }, []);
1136
+ }, [t]);
1126
1137
  const exportOBJ = React.useCallback(() => {
1127
1138
  const root = rootObjectRef.current;
1128
1139
  if (!root) {
1129
- setStatusText('Nothing to export');
1140
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1130
1141
  return;
1131
1142
  }
1132
1143
  const exporter = new OBJExporter_js.OBJExporter();
@@ -1137,12 +1148,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1137
1148
  a.download = 'export.obj';
1138
1149
  a.click();
1139
1150
  URL.revokeObjectURL(a.href);
1140
- setStatusText('Exported OBJ successfully');
1141
- }, []);
1151
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'OBJ'));
1152
+ }, [t]);
1142
1153
  const exportSTL = React.useCallback((binary) => {
1143
1154
  const root = rootObjectRef.current;
1144
1155
  if (!root) {
1145
- setStatusText('Nothing to export');
1156
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1146
1157
  return;
1147
1158
  }
1148
1159
  const exporter = new STLExporter_js.STLExporter();
@@ -1159,12 +1170,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1159
1170
  a.download = 'export.stl';
1160
1171
  a.click();
1161
1172
  URL.revokeObjectURL(a.href);
1162
- setStatusText(`Exported STL${binary ? ' (binary)' : ''} successfully`);
1163
- }, []);
1173
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.stlBinary', 'STL (binary)') : 'STL'));
1174
+ }, [t]);
1164
1175
  const exportPLY = React.useCallback((binary) => {
1165
1176
  const root = rootObjectRef.current;
1166
1177
  if (!root) {
1167
- setStatusText('Nothing to export');
1178
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1168
1179
  return;
1169
1180
  }
1170
1181
  const exporter = new PLYExporter_js.PLYExporter();
@@ -1182,13 +1193,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1182
1193
  a.download = 'export.ply';
1183
1194
  a.click();
1184
1195
  URL.revokeObjectURL(a.href);
1185
- setStatusText(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
1196
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.plyBinary', 'PLY (binary)') : 'PLY'));
1186
1197
  }, { binary });
1187
- }, []);
1198
+ }, [t]);
1188
1199
  const exportUSDZ = React.useCallback(async () => {
1189
1200
  const root = rootObjectRef.current;
1190
1201
  if (!root) {
1191
- setStatusText('Nothing to export');
1202
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1192
1203
  return;
1193
1204
  }
1194
1205
  try {
@@ -1203,13 +1214,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1203
1214
  a.download = 'export.usdz';
1204
1215
  a.click();
1205
1216
  URL.revokeObjectURL(a.href);
1206
- setStatusText('Exported USDZ successfully');
1217
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'USDZ'));
1207
1218
  }
1208
1219
  catch (err) {
1209
1220
  log.error('USDZ export error:', err);
1210
- setStatusText(`USDZ export error: ${String(err)}`);
1221
+ setStatusText(t('useModelEditor.usdzExportError', 'USDZ export error: {err}').replace('{err}', String(String(err))));
1211
1222
  }
1212
- }, []);
1223
+ }, [t]);
1213
1224
  /* ─────────────────────────────────────────
1214
1225
  Dispose & clear
1215
1226
  ───────────────────────────────────────── */
@@ -1247,11 +1258,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1247
1258
  }
1248
1259
  });
1249
1260
  }, []);
1250
- const clearScene = React.useCallback(() => {
1261
+ const clearSceneNow = React.useCallback(() => {
1251
1262
  var _a;
1252
- if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
1253
- return;
1254
- }
1255
1263
  const scene = sceneRef.current;
1256
1264
  const toRemove = [];
1257
1265
  scene.traverse((obj) => {
@@ -1281,8 +1289,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1281
1289
  setAnimDuration(0);
1282
1290
  setSelectedId(null);
1283
1291
  rebuildTree();
1284
- setStatusText('Scene cleared');
1285
- }, [rebuildTree, disposeObject]);
1292
+ setStatusText(t('useModelEditor.sceneCleared', 'Scene cleared'));
1293
+ }, [rebuildTree, disposeObject, t]);
1294
+ // asks first: clearing cannot be undone
1295
+ const clearScene = React.useCallback(() => {
1296
+ void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
1297
+ if (answer !== null) {
1298
+ clearSceneNow();
1299
+ }
1300
+ });
1301
+ }, [ask, clearSceneNow]);
1286
1302
  /* ─────────────────────────────────────────
1287
1303
  Save to Library
1288
1304
  ───────────────────────────────────────── */
@@ -1292,7 +1308,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1292
1308
  }
1293
1309
  const root = rootObjectRef.current;
1294
1310
  if (!root) {
1295
- setStatusText('Nothing to save');
1311
+ setStatusText(t('useModelEditor.nothingToSave', 'Nothing to save'));
1296
1312
  return;
1297
1313
  }
1298
1314
  const exporter = new GLTFExporter_js.GLTFExporter();
@@ -1300,19 +1316,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1300
1316
  const blob = new Blob([result], { type: 'model/gltf-binary' });
1301
1317
  const reader = new FileReader();
1302
1318
  reader.onload = () => {
1303
- const defaultName = 'model.glb';
1304
- const name = window.prompt('Save to library as:', defaultName);
1305
- if (!name || !name.trim()) {
1306
- return;
1307
- }
1308
- onSaveToLibrary(reader.result, name.trim(), 'model/gltf-binary');
1319
+ const dataUrl = reader.result;
1320
+ void ask('prompt', 'Save to library as:', 'model.glb').then((name) => {
1321
+ if (name && name.trim()) {
1322
+ onSaveToLibrary(dataUrl, name.trim(), 'model/gltf-binary');
1323
+ }
1324
+ });
1309
1325
  };
1310
1326
  reader.readAsDataURL(blob);
1311
1327
  }, (err) => {
1312
1328
  log.error('Save to library error:', err);
1313
- setStatusText(`Save error: ${String(err)}`);
1329
+ setStatusText(t('useModelEditor.saveError', 'Save error: {err}').replace('{err}', String(String(err))));
1314
1330
  }, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
1315
- }, [onSaveToLibrary]);
1331
+ }, [onSaveToLibrary, ask, t]);
1316
1332
  /* ─────────────────────────────────────────
1317
1333
  Add Mesh / Light / Camera / Empty
1318
1334
  ───────────────────────────────────────── */
@@ -1379,9 +1395,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1379
1395
  }
1380
1396
  sceneRef.current.add(mesh);
1381
1397
  rebuildTree();
1382
- setStatusText(`Added ${mesh.name}`);
1398
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(mesh.name)));
1383
1399
  setAddMenuOpen(null);
1384
- }, [rebuildTree]);
1400
+ }, [rebuildTree, t]);
1385
1401
  const addSceneLight = React.useCallback((type) => {
1386
1402
  let light;
1387
1403
  switch (type) {
@@ -1433,9 +1449,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1433
1449
  sceneRef.current.add(helper);
1434
1450
  }
1435
1451
  rebuildTree();
1436
- setStatusText(`Added ${light.name}`);
1452
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(light.name)));
1437
1453
  setAddMenuOpen(null);
1438
- }, [rebuildTree]);
1454
+ }, [rebuildTree, t]);
1439
1455
  const addCameraObject = React.useCallback(() => {
1440
1456
  const cam = new THREE__namespace.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
1441
1457
  cam.name = 'Camera';
@@ -1446,9 +1462,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1446
1462
  helper.name = '__helper_Camera';
1447
1463
  sceneRef.current.add(helper);
1448
1464
  rebuildTree();
1449
- setStatusText('Added Camera');
1465
+ setStatusText(t('useModelEditor.addedCamera', 'Added Camera'));
1450
1466
  setAddMenuOpen(null);
1451
- }, [rebuildTree]);
1467
+ }, [rebuildTree, t]);
1452
1468
  const addEmpty = React.useCallback((type) => {
1453
1469
  const obj = new THREE__namespace.Object3D();
1454
1470
  obj.name =
@@ -1460,9 +1476,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1460
1476
  obj.add(h);
1461
1477
  }
1462
1478
  rebuildTree();
1463
- setStatusText(`Added ${obj.name}`);
1479
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(obj.name)));
1464
1480
  setAddMenuOpen(null);
1465
- }, [rebuildTree]);
1481
+ }, [rebuildTree, t]);
1466
1482
  /* ─────────────────────────────────────────
1467
1483
  Raycaster click-to-select
1468
1484
  ───────────────────────────────────────── */
@@ -1530,14 +1546,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1530
1546
  }
1531
1547
  e.target.value = '';
1532
1548
  if (!onAIPoseVideo) {
1533
- setAiStatus('AI pose callback not configured.');
1549
+ setAiStatus(t('useModelEditor.aiPoseCallbackNotConfigured', 'AI pose callback not configured.'));
1534
1550
  return;
1535
1551
  }
1536
1552
  setAiBusy(true);
1537
- setAiStatus('Uploading video for AI pose estimation...');
1553
+ setAiStatus(t('useModelEditor.uploadingVideoForAiPose', 'Uploading video for AI pose estimation...'));
1538
1554
  try {
1539
1555
  const result = await onAIPoseVideo(file);
1540
- setAiStatus(`Got ${result.frame_count} frames at ${result.fps} fps. Creating animation clip...`);
1556
+ setAiStatus(t('useModelEditor.gotFramesAtFpsCreating', 'Got {frame_count} frames at {fps} fps. Creating animation clip...')
1557
+ .replace('{frame_count}', String(result.frame_count))
1558
+ .replace('{fps}', String(result.fps)));
1541
1559
  const clip = modelEditorTypes.pose3dToClip(result);
1542
1560
  if (clip && rootObjectRef.current) {
1543
1561
  if (!mixerRef.current) {
@@ -1552,20 +1570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1552
1570
  allClipsRef.current = [...allClipsRef.current, newAnim];
1553
1571
  setAnimations([...allClipsRef.current]);
1554
1572
  playClip(allClipsRef.current.length - 1);
1555
- setAiStatus('Animation created successfully!');
1573
+ setAiStatus(t('useModelEditor.animationCreatedSuccessfully', 'Animation created successfully!'));
1556
1574
  }
1557
1575
  else {
1558
- setAiStatus('Could not create animation — load a rigged model first.');
1576
+ setAiStatus(t('useModelEditor.couldNotCreateAnimationLoad', 'Could not create animation — load a rigged model first.'));
1559
1577
  }
1560
1578
  }
1561
1579
  catch (err) {
1562
1580
  log.error('AI pose error:', err);
1563
- setAiStatus(`Error: ${String(err)}`);
1581
+ setAiStatus(t('useModelEditor.error', 'Error: {err}').replace('{err}', String(String(err))));
1564
1582
  }
1565
1583
  finally {
1566
1584
  setAiBusy(false);
1567
1585
  }
1568
- }, [playClip, onAIPoseVideo]);
1586
+ }, [playClip, onAIPoseVideo, t]);
1569
1587
  /* ─────────────────────────────────────────
1570
1588
  Node helpers
1571
1589
  ───────────────────────────────────────── */
@@ -1589,19 +1607,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1589
1607
  const undo = React.useCallback(() => {
1590
1608
  const label = undoRedoRef.current.undo();
1591
1609
  if (label) {
1592
- setStatusText(`Undo: ${label}`);
1610
+ setStatusText(t('useModelEditor.undo', 'Undo: {label}').replace('{label}', String(label)));
1593
1611
  rebuildTree();
1594
1612
  }
1595
1613
  setUndoRedoVer((v) => v + 1);
1596
- }, [rebuildTree]);
1614
+ }, [rebuildTree, t]);
1597
1615
  const redo = React.useCallback(() => {
1598
1616
  const label = undoRedoRef.current.redo();
1599
1617
  if (label) {
1600
- setStatusText(`Redo: ${label}`);
1618
+ setStatusText(t('useModelEditor.redo', 'Redo: {label}').replace('{label}', String(label)));
1601
1619
  rebuildTree();
1602
1620
  }
1603
1621
  setUndoRedoVer((v) => v + 1);
1604
- }, [rebuildTree]);
1622
+ }, [rebuildTree, t]);
1605
1623
  const canUndo = undoRedoRef.current.canUndo();
1606
1624
  const canRedo = undoRedoRef.current.canRedo();
1607
1625
  const undoLabel = undoRedoRef.current.undoLabel;
@@ -1630,30 +1648,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1630
1648
  return modelEditorTypes.SUPPORTED_EXTENSIONS.includes(ext);
1631
1649
  });
1632
1650
  if (fileArray.length === 0) {
1633
- setStatusText('No supported files found');
1651
+ setStatusText(t('useModelEditor.noSupportedFilesFound', 'No supported files found'));
1634
1652
  return;
1635
1653
  }
1636
- setStatusText(`Importing ${fileArray.length} file(s)...`);
1654
+ setStatusText(t('useModelEditor.importingFileS', 'Importing {fileArray} file(s)...').replace('{fileArray}', String(fileArray.length)));
1637
1655
  let loaded = 0;
1638
1656
  for (const file of fileArray) {
1639
1657
  try {
1640
1658
  await loadFile(file);
1641
1659
  loaded++;
1642
- setStatusText(`Imported ${loaded}/${fileArray.length}: ${file.name}`);
1660
+ setStatusText(t('useModelEditor.imported', 'Imported {loaded}/{fileArray}: {name}')
1661
+ .replace('{loaded}', String(loaded))
1662
+ .replace('{fileArray}', String(fileArray.length))
1663
+ .replace('{name}', String(file.name)));
1643
1664
  }
1644
1665
  catch (err) {
1645
1666
  log.error(`Failed to import ${file.name}:`, err);
1646
1667
  }
1647
1668
  }
1648
- setStatusText(`Batch import complete: ${loaded}/${fileArray.length} files loaded`);
1649
- }, [loadFile]);
1669
+ setStatusText(t('useModelEditor.batchImportCompleteFilesLoaded', 'Batch import complete: {loaded}/{fileArray} files loaded')
1670
+ .replace('{loaded}', String(loaded))
1671
+ .replace('{fileArray}', String(fileArray.length)));
1672
+ }, [loadFile, t]);
1650
1673
  /* ─────────────────────────────────────────
1651
1674
  PBR Presets
1652
1675
  ───────────────────────────────────────── */
1653
1676
  const applyPreset = React.useCallback((preset) => {
1654
1677
  const node = selectedNode;
1655
1678
  if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1656
- setStatusText('Select a mesh to apply material preset');
1679
+ setStatusText(t('useModelEditor.selectAMeshToApply', 'Select a mesh to apply material preset'));
1657
1680
  return;
1658
1681
  }
1659
1682
  const mesh = node.object;
@@ -1686,7 +1709,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1686
1709
  mesh.material = newMaterial;
1687
1710
  }
1688
1711
  pushUndo({
1689
- label: `Apply ${preset.name} preset`,
1712
+ label: t('useModelEditor.applyPreset', 'Apply {name} preset').replace('{name}', String(preset.name)),
1690
1713
  undo: () => {
1691
1714
  if (Array.isArray(mesh.material)) {
1692
1715
  mesh.material[materialIdx] = oldClone;
@@ -1705,15 +1728,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1705
1728
  },
1706
1729
  });
1707
1730
  setMatRefresh((v) => v + 1);
1708
- setStatusText(`Applied ${preset.name} preset`);
1709
- }, [selectedNode, selMaterialIdx, pushUndo]);
1731
+ setStatusText(t('useModelEditor.appliedPreset', 'Applied {name} preset').replace('{name}', String(preset.name)));
1732
+ }, [selectedNode, selMaterialIdx, pushUndo, t]);
1710
1733
  /* ─────────────────────────────────────────
1711
1734
  LOD Generation
1712
1735
  ───────────────────────────────────────── */
1713
1736
  const generateSelectedLOD = React.useCallback(() => {
1714
1737
  const node = selectedNode;
1715
1738
  if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1716
- setStatusText('Select a mesh to generate LOD');
1739
+ setStatusText(t('useModelEditor.selectAMeshToGenerate', 'Select a mesh to generate LOD'));
1717
1740
  return;
1718
1741
  }
1719
1742
  const mesh = node.object;
@@ -1722,7 +1745,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1722
1745
  parent.remove(mesh);
1723
1746
  parent.add(lod);
1724
1747
  pushUndo({
1725
- label: `Generate LOD for ${mesh.name}`,
1748
+ label: t('useModelEditor.generateLodFor', 'Generate LOD for {name}').replace('{name}', String(mesh.name)),
1726
1749
  undo: () => {
1727
1750
  parent.remove(lod);
1728
1751
  parent.add(mesh);
@@ -1733,8 +1756,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1733
1756
  },
1734
1757
  });
1735
1758
  rebuildTree();
1736
- setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
1737
- }, [selectedNode, pushUndo, rebuildTree]);
1759
+ setStatusText(t('useModelEditor.generatedLodFor4Levels', 'Generated LOD for {name} (4 levels)').replace('{name}', String(mesh.name)));
1760
+ }, [selectedNode, pushUndo, rebuildTree, t]);
1738
1761
  /* ─────────────────────────────────────────
1739
1762
  Instancing
1740
1763
  ───────────────────────────────────────── */
@@ -1742,12 +1765,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1742
1765
  const root = rootObjectRef.current || sceneRef.current;
1743
1766
  const result = modelEditorUtils.convertToInstanced(root);
1744
1767
  if (result.created === 0) {
1745
- setStatusText('No duplicate meshes found to instance');
1768
+ setStatusText(t('useModelEditor.noDuplicateMeshesFoundTo', 'No duplicate meshes found to instance'));
1746
1769
  return;
1747
1770
  }
1748
1771
  rebuildTree();
1749
- setStatusText(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
1750
- }, [rebuildTree]);
1772
+ setStatusText(t('useModelEditor.createdInstancedMeshesRemovedDuplicates', 'Created {created} instanced meshes, removed {removed} duplicates')
1773
+ .replace('{created}', String(result.created))
1774
+ .replace('{removed}', String(result.removed)));
1775
+ }, [rebuildTree, t]);
1751
1776
  /* ── HDRI Environment ── */
1752
1777
  const applyHDRI = React.useCallback((preset) => {
1753
1778
  const renderer = rendererRef.current;
@@ -1765,8 +1790,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1765
1790
  else {
1766
1791
  setFogEnabled(false);
1767
1792
  }
1768
- setStatusText(`Environment: ${preset.name}`);
1769
- }, []);
1793
+ setStatusText(t('useModelEditor.environment', 'Environment: {name}').replace('{name}', String(preset.name)));
1794
+ }, [t]);
1770
1795
  /* ── Snap ── */
1771
1796
  const snapPoint = React.useCallback((point) => {
1772
1797
  const root = rootObjectRef.current || sceneRef.current;
@@ -1808,6 +1833,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1808
1833
  bgColor,
1809
1834
  setBgColor,
1810
1835
  statusText,
1836
+ pendingAsk,
1811
1837
  dragOver,
1812
1838
  polyCount,
1813
1839
  meshCount,
@@ -10,6 +10,7 @@ var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
10
10
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
11
11
  var PLYLoader_js = require('three/examples/jsm/loaders/PLYLoader.js');
12
12
  var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
13
+ var i18n = require('../core/i18n.js');
13
14
  var threeCompat = require('../core/threeCompat.js');
14
15
 
15
16
  function _interopNamespaceDefault(e) {
@@ -62,6 +63,7 @@ const TONE_MAP = {
62
63
  ═══════════════════════════════════════════ */
63
64
  function useModelViewer(props = {}) {
64
65
  var _a;
66
+ const { t } = i18n.useNiceTranslation();
65
67
  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;
66
68
  /* ── refs ── */
67
69
  const mountRef = React.useRef(null);
@@ -349,7 +351,7 @@ function useModelViewer(props = {}) {
349
351
  setLoading(true);
350
352
  setLoadProgress(0);
351
353
  setError(null);
352
- setStatusText('Loading…');
354
+ setStatusText(t('useModelViewer.loading', 'Loading…'));
353
355
  const detectedFmt = detectFormat(url, fmt);
354
356
  const onProgress = (e) => {
355
357
  if (e.lengthComputable) {
@@ -457,21 +459,23 @@ function useModelViewer(props = {}) {
457
459
  mixerRef.current = mixer;
458
460
  }
459
461
  countScene();
460
- setStatusText(`Loaded – ${polyCount} tris, ${meshCount} meshes`);
462
+ setStatusText(t('useModelViewer.loadedTrisMeshes', 'Loaded – {polyCount} tris, {meshCount} meshes')
463
+ .replace('{polyCount}', String(polyCount))
464
+ .replace('{meshCount}', String(meshCount)));
461
465
  onLoad === null || onLoad === void 0 ? void 0 : onLoad();
462
466
  }
463
467
  }
464
468
  catch (err) {
465
469
  const msg = err instanceof Error ? err.message : String(err);
466
470
  setError(msg);
467
- setStatusText(`Error: ${msg}`);
471
+ setStatusText(t('useModelViewer.error', 'Error: {msg}').replace('{msg}', String(msg)));
468
472
  onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(msg));
469
473
  }
470
474
  finally {
471
475
  setLoading(false);
472
476
  setLoadProgress(1);
473
477
  }
474
- }, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount]);
478
+ }, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount, t]);
475
479
  /* ── auto-load from src ── */
476
480
  React.useEffect(() => {
477
481
  if (src) {
@@ -612,9 +616,12 @@ function useModelViewer(props = {}) {
612
616
  /* ── finalise status text ── */
613
617
  React.useEffect(() => {
614
618
  if (!loading && !error && polyCount > 0) {
615
- setStatusText(`${polyCount.toLocaleString()} triangles · ${meshCount} meshes · ${animNames.length} animation(s)`);
619
+ setStatusText(t('useModelViewer.trianglesMeshesAnimationS', '{polyCount} triangles · {meshCount} meshes · {animNames} animation(s)')
620
+ .replace('{polyCount}', String(polyCount.toLocaleString()))
621
+ .replace('{meshCount}', String(meshCount))
622
+ .replace('{animNames}', String(animNames.length)));
616
623
  }
617
- }, [loading, error, polyCount, meshCount, animNames.length]);
624
+ }, [loading, error, polyCount, meshCount, animNames.length, t]);
618
625
  return {
619
626
  mountRef,
620
627
  loading,