@nice2dev/ui-3d 1.2.1 → 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 (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/cjs/material/NiceMaterialEditor.js +1 -0
  3. package/dist/cjs/model/ModelEditor.js +7 -1
  4. package/dist/cjs/model/ModelEditorMenuBar.js +5 -5
  5. package/dist/cjs/model/ModelEditorRightPanel.js +13 -9
  6. package/dist/cjs/model/ModelEditorSubComponents.js +9 -3
  7. package/dist/cjs/model/ModelEditorTimeline.js +3 -1
  8. package/dist/cjs/model/ModelEditorToolbar.js +1 -1
  9. package/dist/cjs/model/ModelEditorViewport.js +5 -2
  10. package/dist/cjs/model/ModelViewer.js +4 -4
  11. package/dist/cjs/model/NiceArmatureEditor.js +4 -4
  12. package/dist/cjs/model/NiceMorphTargetEditor.js +3 -1
  13. package/dist/cjs/model/NiceOctree.js +7 -4
  14. package/dist/cjs/model/NicePhysicsSimulation.js +4 -4
  15. package/dist/cjs/model/NiceProceduralGeometry.js +3 -3
  16. package/dist/cjs/model/NiceTerrainEditor.js +2 -2
  17. package/dist/cjs/model/NiceWeightPainter.js +2 -2
  18. package/dist/cjs/model/NiceXRPreview.js +6 -3
  19. package/dist/cjs/model/useModelEditor.js +92 -73
  20. package/dist/cjs/model/useModelViewer.js +13 -6
  21. package/dist/cjs/particle/NiceParticleEditor.js +10 -3
  22. package/dist/cjs/rendering/NiceCascadedShadows.js +1 -1
  23. package/dist/cjs/rendering/NiceRenderExport.js +2 -2
  24. package/dist/cjs/rendering/NiceSSAO.js +2 -1
  25. package/dist/cjs/rendering/NiceSSR.js +2 -1
  26. package/dist/cjs/rendering/NiceWebGPURenderer.js +2 -1
  27. package/dist/cjs/uv/NiceUVEditor.js +3 -3
  28. package/dist/esm/material/NiceMaterialEditor.js +1 -0
  29. package/dist/esm/model/ModelEditor.js +7 -1
  30. package/dist/esm/model/ModelEditorMenuBar.js +5 -5
  31. package/dist/esm/model/ModelEditorRightPanel.js +13 -9
  32. package/dist/esm/model/ModelEditorSubComponents.js +9 -3
  33. package/dist/esm/model/ModelEditorTimeline.js +3 -1
  34. package/dist/esm/model/ModelEditorToolbar.js +1 -1
  35. package/dist/esm/model/ModelEditorViewport.js +5 -2
  36. package/dist/esm/model/ModelViewer.js +4 -4
  37. package/dist/esm/model/NiceArmatureEditor.js +4 -4
  38. package/dist/esm/model/NiceMorphTargetEditor.js +3 -1
  39. package/dist/esm/model/NiceOctree.js +7 -4
  40. package/dist/esm/model/NicePhysicsSimulation.js +4 -4
  41. package/dist/esm/model/NiceProceduralGeometry.js +3 -3
  42. package/dist/esm/model/NiceTerrainEditor.js +2 -2
  43. package/dist/esm/model/NiceWeightPainter.js +2 -2
  44. package/dist/esm/model/NiceXRPreview.js +6 -3
  45. package/dist/esm/model/useModelEditor.js +92 -73
  46. package/dist/esm/model/useModelViewer.js +13 -6
  47. package/dist/esm/particle/NiceParticleEditor.js +10 -3
  48. package/dist/esm/rendering/NiceCascadedShadows.js +1 -1
  49. package/dist/esm/rendering/NiceRenderExport.js +2 -2
  50. package/dist/esm/rendering/NiceSSAO.js +2 -1
  51. package/dist/esm/rendering/NiceSSR.js +2 -1
  52. package/dist/esm/rendering/NiceWebGPURenderer.js +2 -1
  53. package/dist/esm/uv/NiceUVEditor.js +3 -3
  54. package/package.json +2 -2
@@ -24,6 +24,7 @@ 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
26
  var editorAsk = require('../core/editorAsk.js');
27
+ var i18n = require('../core/i18n.js');
27
28
  var threeCompat = require('../core/threeCompat.js');
28
29
  var modelEditorTypes = require('./modelEditorTypes.js');
29
30
  var modelEditorUtils = require('./modelEditorUtils.js');
@@ -76,6 +77,7 @@ function findNodeById(nodes, id) {
76
77
  ═══════════════════════════════════════════ */
77
78
  function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
78
79
  var _a;
80
+ const { t } = i18n.useNiceTranslation();
79
81
  const { pendingAsk, ask } = editorAsk.useEditorAsk();
80
82
  /* ── refs ── */
81
83
  const mountRef = React.useRef(null);
@@ -415,14 +417,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
415
417
  }
416
418
  focusOnObjectRef.current(obj);
417
419
  rebuildTree();
418
- setStatusText(`Loaded: ${filename} (${clips.length} animation(s))`);
419
- }, [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]);
420
424
  const loadFile = React.useCallback(async (file) => {
421
425
  var _a, _b, _c, _d, _e, _f;
422
426
  const name = file.name.toLowerCase();
423
427
  const ext = '.' + name.split('.').pop();
424
428
  const url = URL.createObjectURL(file);
425
- setStatusText(`Loading ${file.name}...`);
429
+ setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
426
430
  try {
427
431
  if (ext === '.fbx') {
428
432
  const loader = new FBXLoader_js.FBXLoader();
@@ -546,32 +550,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
546
550
  addObjectToScene(group, [], file.name);
547
551
  }
548
552
  else {
549
- setStatusText(`Unsupported format: ${ext}`);
553
+ setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
550
554
  }
551
555
  }
552
556
  catch (err) {
553
557
  log.error('Load error:', err);
554
- 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))));
555
561
  }
556
562
  finally {
557
563
  URL.revokeObjectURL(url);
558
564
  }
559
- }, [addObjectToScene]);
565
+ }, [addObjectToScene, t]);
560
566
  const mergeAnimationFBX = React.useCallback(async (file) => {
561
567
  var _a;
562
568
  const url = URL.createObjectURL(file);
563
- setStatusText(`Merging animations from ${file.name}...`);
569
+ setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
564
570
  try {
565
571
  const loader = new FBXLoader_js.FBXLoader();
566
572
  const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
567
573
  const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
568
574
  if (clips.length === 0) {
569
- setStatusText(`No animations found in ${file.name}`);
575
+ setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
570
576
  return;
571
577
  }
572
578
  const root = rootObjectRef.current;
573
579
  if (!root) {
574
- setStatusText('No model loaded — load a base model first');
580
+ setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
575
581
  return;
576
582
  }
577
583
  if (!mixerRef.current) {
@@ -585,16 +591,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
585
591
  }));
586
592
  allClipsRef.current = [...allClipsRef.current, ...newAnims];
587
593
  setAnimations([...allClipsRef.current]);
588
- 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)));
589
597
  }
590
598
  catch (err) {
591
599
  log.error('Merge error:', err);
592
- 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))));
593
603
  }
594
604
  finally {
595
605
  URL.revokeObjectURL(url);
596
606
  }
597
- }, []);
607
+ }, [t]);
598
608
  /* ─────────────────────────────────────────
599
609
  Animation controls
600
610
  ───────────────────────────────────────── */
@@ -952,8 +962,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
952
962
  clone.position.x += 1;
953
963
  sceneRef.current.add(clone);
954
964
  rebuildTree();
955
- setStatusText(`Duplicated ${selectedNode.name}`);
956
- }, [selectedNode, rebuildTree]);
965
+ setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
966
+ }, [selectedNode, rebuildTree, t]);
957
967
  /* ─────────────────────────────────────────
958
968
  Keyboard shortcuts
959
969
  ───────────────────────────────────────── */
@@ -1097,7 +1107,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1097
1107
  const exportGLTF = React.useCallback((binary) => {
1098
1108
  const root = rootObjectRef.current;
1099
1109
  if (!root) {
1100
- setStatusText('Nothing to export');
1110
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1101
1111
  return;
1102
1112
  }
1103
1113
  const exporter = new GLTFExporter_js.GLTFExporter();
@@ -1118,16 +1128,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1118
1128
  a.download = `export.${binary ? 'glb' : 'gltf'}`;
1119
1129
  a.click();
1120
1130
  URL.revokeObjectURL(a.href);
1121
- setStatusText(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
1131
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? 'GLB' : 'GLTF'));
1122
1132
  }, (err) => {
1123
1133
  log.error('Export error:', err);
1124
- setStatusText(`Export error: ${String(err)}`);
1134
+ setStatusText(t('useModelEditor.exportError', 'Export error: {err}').replace('{err}', String(String(err))));
1125
1135
  }, options);
1126
- }, []);
1136
+ }, [t]);
1127
1137
  const exportOBJ = React.useCallback(() => {
1128
1138
  const root = rootObjectRef.current;
1129
1139
  if (!root) {
1130
- setStatusText('Nothing to export');
1140
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1131
1141
  return;
1132
1142
  }
1133
1143
  const exporter = new OBJExporter_js.OBJExporter();
@@ -1138,12 +1148,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1138
1148
  a.download = 'export.obj';
1139
1149
  a.click();
1140
1150
  URL.revokeObjectURL(a.href);
1141
- setStatusText('Exported OBJ successfully');
1142
- }, []);
1151
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'OBJ'));
1152
+ }, [t]);
1143
1153
  const exportSTL = React.useCallback((binary) => {
1144
1154
  const root = rootObjectRef.current;
1145
1155
  if (!root) {
1146
- setStatusText('Nothing to export');
1156
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1147
1157
  return;
1148
1158
  }
1149
1159
  const exporter = new STLExporter_js.STLExporter();
@@ -1160,12 +1170,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1160
1170
  a.download = 'export.stl';
1161
1171
  a.click();
1162
1172
  URL.revokeObjectURL(a.href);
1163
- setStatusText(`Exported STL${binary ? ' (binary)' : ''} successfully`);
1164
- }, []);
1173
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.stlBinary', 'STL (binary)') : 'STL'));
1174
+ }, [t]);
1165
1175
  const exportPLY = React.useCallback((binary) => {
1166
1176
  const root = rootObjectRef.current;
1167
1177
  if (!root) {
1168
- setStatusText('Nothing to export');
1178
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1169
1179
  return;
1170
1180
  }
1171
1181
  const exporter = new PLYExporter_js.PLYExporter();
@@ -1183,13 +1193,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1183
1193
  a.download = 'export.ply';
1184
1194
  a.click();
1185
1195
  URL.revokeObjectURL(a.href);
1186
- setStatusText(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
1196
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.plyBinary', 'PLY (binary)') : 'PLY'));
1187
1197
  }, { binary });
1188
- }, []);
1198
+ }, [t]);
1189
1199
  const exportUSDZ = React.useCallback(async () => {
1190
1200
  const root = rootObjectRef.current;
1191
1201
  if (!root) {
1192
- setStatusText('Nothing to export');
1202
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1193
1203
  return;
1194
1204
  }
1195
1205
  try {
@@ -1204,13 +1214,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1204
1214
  a.download = 'export.usdz';
1205
1215
  a.click();
1206
1216
  URL.revokeObjectURL(a.href);
1207
- setStatusText('Exported USDZ successfully');
1217
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'USDZ'));
1208
1218
  }
1209
1219
  catch (err) {
1210
1220
  log.error('USDZ export error:', err);
1211
- setStatusText(`USDZ export error: ${String(err)}`);
1221
+ setStatusText(t('useModelEditor.usdzExportError', 'USDZ export error: {err}').replace('{err}', String(String(err))));
1212
1222
  }
1213
- }, []);
1223
+ }, [t]);
1214
1224
  /* ─────────────────────────────────────────
1215
1225
  Dispose & clear
1216
1226
  ───────────────────────────────────────── */
@@ -1279,8 +1289,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1279
1289
  setAnimDuration(0);
1280
1290
  setSelectedId(null);
1281
1291
  rebuildTree();
1282
- setStatusText('Scene cleared');
1283
- }, [rebuildTree, disposeObject]);
1292
+ setStatusText(t('useModelEditor.sceneCleared', 'Scene cleared'));
1293
+ }, [rebuildTree, disposeObject, t]);
1284
1294
  // asks first: clearing cannot be undone
1285
1295
  const clearScene = React.useCallback(() => {
1286
1296
  void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
@@ -1298,7 +1308,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1298
1308
  }
1299
1309
  const root = rootObjectRef.current;
1300
1310
  if (!root) {
1301
- setStatusText('Nothing to save');
1311
+ setStatusText(t('useModelEditor.nothingToSave', 'Nothing to save'));
1302
1312
  return;
1303
1313
  }
1304
1314
  const exporter = new GLTFExporter_js.GLTFExporter();
@@ -1316,9 +1326,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1316
1326
  reader.readAsDataURL(blob);
1317
1327
  }, (err) => {
1318
1328
  log.error('Save to library error:', err);
1319
- setStatusText(`Save error: ${String(err)}`);
1329
+ setStatusText(t('useModelEditor.saveError', 'Save error: {err}').replace('{err}', String(String(err))));
1320
1330
  }, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
1321
- }, [onSaveToLibrary, ask]);
1331
+ }, [onSaveToLibrary, ask, t]);
1322
1332
  /* ─────────────────────────────────────────
1323
1333
  Add Mesh / Light / Camera / Empty
1324
1334
  ───────────────────────────────────────── */
@@ -1385,9 +1395,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1385
1395
  }
1386
1396
  sceneRef.current.add(mesh);
1387
1397
  rebuildTree();
1388
- setStatusText(`Added ${mesh.name}`);
1398
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(mesh.name)));
1389
1399
  setAddMenuOpen(null);
1390
- }, [rebuildTree]);
1400
+ }, [rebuildTree, t]);
1391
1401
  const addSceneLight = React.useCallback((type) => {
1392
1402
  let light;
1393
1403
  switch (type) {
@@ -1439,9 +1449,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1439
1449
  sceneRef.current.add(helper);
1440
1450
  }
1441
1451
  rebuildTree();
1442
- setStatusText(`Added ${light.name}`);
1452
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(light.name)));
1443
1453
  setAddMenuOpen(null);
1444
- }, [rebuildTree]);
1454
+ }, [rebuildTree, t]);
1445
1455
  const addCameraObject = React.useCallback(() => {
1446
1456
  const cam = new THREE__namespace.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
1447
1457
  cam.name = 'Camera';
@@ -1452,9 +1462,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1452
1462
  helper.name = '__helper_Camera';
1453
1463
  sceneRef.current.add(helper);
1454
1464
  rebuildTree();
1455
- setStatusText('Added Camera');
1465
+ setStatusText(t('useModelEditor.addedCamera', 'Added Camera'));
1456
1466
  setAddMenuOpen(null);
1457
- }, [rebuildTree]);
1467
+ }, [rebuildTree, t]);
1458
1468
  const addEmpty = React.useCallback((type) => {
1459
1469
  const obj = new THREE__namespace.Object3D();
1460
1470
  obj.name =
@@ -1466,9 +1476,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1466
1476
  obj.add(h);
1467
1477
  }
1468
1478
  rebuildTree();
1469
- setStatusText(`Added ${obj.name}`);
1479
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(obj.name)));
1470
1480
  setAddMenuOpen(null);
1471
- }, [rebuildTree]);
1481
+ }, [rebuildTree, t]);
1472
1482
  /* ─────────────────────────────────────────
1473
1483
  Raycaster click-to-select
1474
1484
  ───────────────────────────────────────── */
@@ -1536,14 +1546,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1536
1546
  }
1537
1547
  e.target.value = '';
1538
1548
  if (!onAIPoseVideo) {
1539
- setAiStatus('AI pose callback not configured.');
1549
+ setAiStatus(t('useModelEditor.aiPoseCallbackNotConfigured', 'AI pose callback not configured.'));
1540
1550
  return;
1541
1551
  }
1542
1552
  setAiBusy(true);
1543
- setAiStatus('Uploading video for AI pose estimation...');
1553
+ setAiStatus(t('useModelEditor.uploadingVideoForAiPose', 'Uploading video for AI pose estimation...'));
1544
1554
  try {
1545
1555
  const result = await onAIPoseVideo(file);
1546
- 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)));
1547
1559
  const clip = modelEditorTypes.pose3dToClip(result);
1548
1560
  if (clip && rootObjectRef.current) {
1549
1561
  if (!mixerRef.current) {
@@ -1558,20 +1570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1558
1570
  allClipsRef.current = [...allClipsRef.current, newAnim];
1559
1571
  setAnimations([...allClipsRef.current]);
1560
1572
  playClip(allClipsRef.current.length - 1);
1561
- setAiStatus('Animation created successfully!');
1573
+ setAiStatus(t('useModelEditor.animationCreatedSuccessfully', 'Animation created successfully!'));
1562
1574
  }
1563
1575
  else {
1564
- setAiStatus('Could not create animation — load a rigged model first.');
1576
+ setAiStatus(t('useModelEditor.couldNotCreateAnimationLoad', 'Could not create animation — load a rigged model first.'));
1565
1577
  }
1566
1578
  }
1567
1579
  catch (err) {
1568
1580
  log.error('AI pose error:', err);
1569
- setAiStatus(`Error: ${String(err)}`);
1581
+ setAiStatus(t('useModelEditor.error', 'Error: {err}').replace('{err}', String(String(err))));
1570
1582
  }
1571
1583
  finally {
1572
1584
  setAiBusy(false);
1573
1585
  }
1574
- }, [playClip, onAIPoseVideo]);
1586
+ }, [playClip, onAIPoseVideo, t]);
1575
1587
  /* ─────────────────────────────────────────
1576
1588
  Node helpers
1577
1589
  ───────────────────────────────────────── */
@@ -1595,19 +1607,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1595
1607
  const undo = React.useCallback(() => {
1596
1608
  const label = undoRedoRef.current.undo();
1597
1609
  if (label) {
1598
- setStatusText(`Undo: ${label}`);
1610
+ setStatusText(t('useModelEditor.undo', 'Undo: {label}').replace('{label}', String(label)));
1599
1611
  rebuildTree();
1600
1612
  }
1601
1613
  setUndoRedoVer((v) => v + 1);
1602
- }, [rebuildTree]);
1614
+ }, [rebuildTree, t]);
1603
1615
  const redo = React.useCallback(() => {
1604
1616
  const label = undoRedoRef.current.redo();
1605
1617
  if (label) {
1606
- setStatusText(`Redo: ${label}`);
1618
+ setStatusText(t('useModelEditor.redo', 'Redo: {label}').replace('{label}', String(label)));
1607
1619
  rebuildTree();
1608
1620
  }
1609
1621
  setUndoRedoVer((v) => v + 1);
1610
- }, [rebuildTree]);
1622
+ }, [rebuildTree, t]);
1611
1623
  const canUndo = undoRedoRef.current.canUndo();
1612
1624
  const canRedo = undoRedoRef.current.canRedo();
1613
1625
  const undoLabel = undoRedoRef.current.undoLabel;
@@ -1636,30 +1648,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1636
1648
  return modelEditorTypes.SUPPORTED_EXTENSIONS.includes(ext);
1637
1649
  });
1638
1650
  if (fileArray.length === 0) {
1639
- setStatusText('No supported files found');
1651
+ setStatusText(t('useModelEditor.noSupportedFilesFound', 'No supported files found'));
1640
1652
  return;
1641
1653
  }
1642
- setStatusText(`Importing ${fileArray.length} file(s)...`);
1654
+ setStatusText(t('useModelEditor.importingFileS', 'Importing {fileArray} file(s)...').replace('{fileArray}', String(fileArray.length)));
1643
1655
  let loaded = 0;
1644
1656
  for (const file of fileArray) {
1645
1657
  try {
1646
1658
  await loadFile(file);
1647
1659
  loaded++;
1648
- 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)));
1649
1664
  }
1650
1665
  catch (err) {
1651
1666
  log.error(`Failed to import ${file.name}:`, err);
1652
1667
  }
1653
1668
  }
1654
- setStatusText(`Batch import complete: ${loaded}/${fileArray.length} files loaded`);
1655
- }, [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]);
1656
1673
  /* ─────────────────────────────────────────
1657
1674
  PBR Presets
1658
1675
  ───────────────────────────────────────── */
1659
1676
  const applyPreset = React.useCallback((preset) => {
1660
1677
  const node = selectedNode;
1661
1678
  if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1662
- setStatusText('Select a mesh to apply material preset');
1679
+ setStatusText(t('useModelEditor.selectAMeshToApply', 'Select a mesh to apply material preset'));
1663
1680
  return;
1664
1681
  }
1665
1682
  const mesh = node.object;
@@ -1692,7 +1709,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1692
1709
  mesh.material = newMaterial;
1693
1710
  }
1694
1711
  pushUndo({
1695
- label: `Apply ${preset.name} preset`,
1712
+ label: t('useModelEditor.applyPreset', 'Apply {name} preset').replace('{name}', String(preset.name)),
1696
1713
  undo: () => {
1697
1714
  if (Array.isArray(mesh.material)) {
1698
1715
  mesh.material[materialIdx] = oldClone;
@@ -1711,15 +1728,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1711
1728
  },
1712
1729
  });
1713
1730
  setMatRefresh((v) => v + 1);
1714
- setStatusText(`Applied ${preset.name} preset`);
1715
- }, [selectedNode, selMaterialIdx, pushUndo]);
1731
+ setStatusText(t('useModelEditor.appliedPreset', 'Applied {name} preset').replace('{name}', String(preset.name)));
1732
+ }, [selectedNode, selMaterialIdx, pushUndo, t]);
1716
1733
  /* ─────────────────────────────────────────
1717
1734
  LOD Generation
1718
1735
  ───────────────────────────────────────── */
1719
1736
  const generateSelectedLOD = React.useCallback(() => {
1720
1737
  const node = selectedNode;
1721
1738
  if (!node || !(node.object instanceof THREE__namespace.Mesh)) {
1722
- setStatusText('Select a mesh to generate LOD');
1739
+ setStatusText(t('useModelEditor.selectAMeshToGenerate', 'Select a mesh to generate LOD'));
1723
1740
  return;
1724
1741
  }
1725
1742
  const mesh = node.object;
@@ -1728,7 +1745,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1728
1745
  parent.remove(mesh);
1729
1746
  parent.add(lod);
1730
1747
  pushUndo({
1731
- label: `Generate LOD for ${mesh.name}`,
1748
+ label: t('useModelEditor.generateLodFor', 'Generate LOD for {name}').replace('{name}', String(mesh.name)),
1732
1749
  undo: () => {
1733
1750
  parent.remove(lod);
1734
1751
  parent.add(mesh);
@@ -1739,8 +1756,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1739
1756
  },
1740
1757
  });
1741
1758
  rebuildTree();
1742
- setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
1743
- }, [selectedNode, pushUndo, rebuildTree]);
1759
+ setStatusText(t('useModelEditor.generatedLodFor4Levels', 'Generated LOD for {name} (4 levels)').replace('{name}', String(mesh.name)));
1760
+ }, [selectedNode, pushUndo, rebuildTree, t]);
1744
1761
  /* ─────────────────────────────────────────
1745
1762
  Instancing
1746
1763
  ───────────────────────────────────────── */
@@ -1748,12 +1765,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1748
1765
  const root = rootObjectRef.current || sceneRef.current;
1749
1766
  const result = modelEditorUtils.convertToInstanced(root);
1750
1767
  if (result.created === 0) {
1751
- setStatusText('No duplicate meshes found to instance');
1768
+ setStatusText(t('useModelEditor.noDuplicateMeshesFoundTo', 'No duplicate meshes found to instance'));
1752
1769
  return;
1753
1770
  }
1754
1771
  rebuildTree();
1755
- setStatusText(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
1756
- }, [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]);
1757
1776
  /* ── HDRI Environment ── */
1758
1777
  const applyHDRI = React.useCallback((preset) => {
1759
1778
  const renderer = rendererRef.current;
@@ -1771,8 +1790,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1771
1790
  else {
1772
1791
  setFogEnabled(false);
1773
1792
  }
1774
- setStatusText(`Environment: ${preset.name}`);
1775
- }, []);
1793
+ setStatusText(t('useModelEditor.environment', 'Environment: {name}').replace('{name}', String(preset.name)));
1794
+ }, [t]);
1776
1795
  /* ── Snap ── */
1777
1796
  const snapPoint = React.useCallback((point) => {
1778
1797
  const root = rootObjectRef.current || sceneRef.current;
@@ -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,
@@ -297,7 +297,7 @@ function SystemListPanel({ systems, selectedId, onSelect, onAdd, onRemove, onDup
297
297
  }
298
298
  setEditingId(null);
299
299
  };
300
- return (jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemList, children: [jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListHeader, children: [jsxRuntime.jsxs("span", { children: ["Systems (", systems.length, ")"] }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListActions, children: [jsxRuntime.jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "plus", size: 14 }) }), jsxRuntime.jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsxRuntime.jsx("div", { className: ParticleEditor_module.presetMenu, children: Object.keys(particleEditorUtils.PARTICLE_PRESETS).map((preset) => (jsxRuntime.jsx("button", { className: ParticleEditor_module.presetItem, onClick: () => {
300
+ return (jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemList, children: [jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListHeader, children: [jsxRuntime.jsx("span", { children: t('particleEditor.systems', 'Systems ({systems})').replace('{systems}', String(systems.length)) }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListActions, children: [jsxRuntime.jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "plus", size: 14 }) }), jsxRuntime.jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsxRuntime.jsx("div", { className: ParticleEditor_module.presetMenu, children: Object.keys(particleEditorUtils.PARTICLE_PRESETS).map((preset) => (jsxRuntime.jsx("button", { className: ParticleEditor_module.presetItem, onClick: () => {
301
301
  onAddPreset(preset);
302
302
  setShowPresets(false);
303
303
  }, children: preset.charAt(0).toUpperCase() + preset.slice(1) }, preset))) })), jsxRuntime.jsx("div", { className: ParticleEditor_module.systemItems, children: systems.map((system) => (jsxRuntime.jsx("div", { className: `${ParticleEditor_module.systemItem} ${selectedId === system.id ? ParticleEditor_module.selected : ''}`, onClick: () => onSelect(system.id), children: editingId === system.id ? (jsxRuntime.jsx("input", { type: "text", value: editName, onChange: (e) => setEditName(e.target.value), onBlur: handleFinishRename, onKeyDown: (e) => e.key === 'Enter' && handleFinishRename(), autoFocus: true, className: ParticleEditor_module.renameInput })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: ParticleEditor_module.systemName, onDoubleClick: () => handleStartRename(system), children: system.name }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemItemActions, children: [jsxRuntime.jsx("button", { onClick: (e) => {
@@ -385,7 +385,7 @@ function ModuleEditor({ system, moduleKey, onUpdate }) {
385
385
  step: 0.1,
386
386
  }), renderToggle('Looping', system.looping, (v) => onUpdate({ looping: v })), renderToggle('Prewarm', system.prewarm, (v) => onUpdate({ prewarm: v })), renderDistributionInput('Start Delay', system.startDelay, (v) => onUpdate({ startDelay: v })), renderDistributionInput('Start Lifetime', system.startLifetime, (v) => onUpdate({ startLifetime: v })), renderDistributionInput('Start Speed', system.startSpeed, (v) => onUpdate({ startSpeed: v })), renderDistributionInput('Start Size', system.startSize, (v) => onUpdate({ startSize: v })), renderDistributionInput('Start Rotation', system.startRotation, (v) => onUpdate({ startRotation: v })), renderDistributionInput('Gravity Modifier', system.gravityModifier, (v) => onUpdate({ gravityModifier: v })), renderSelect('Simulation Space', system.simulationSpace, { local: 'Local', world: 'World', custom: 'Custom' }, (v) => onUpdate({ simulationSpace: v })), renderNumberInput('Simulation Speed', system.simulationSpeed, (v) => onUpdate({ simulationSpeed: v }), { min: 0, step: 0.1 }), renderNumberInput('Max Particles', system.maxParticles, (v) => onUpdate({ maxParticles: v }), { min: 1, step: 1 })] }));
387
387
  case 'emission':
388
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderDistributionInput('Rate over Time', system.emission.rateOverTime, (v) => onUpdate({ emission: { ...system.emission, rateOverTime: v } })), renderDistributionInput('Rate over Distance', system.emission.rateOverDistance, (v) => onUpdate({ emission: { ...system.emission, rateOverDistance: v } })), jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsxs("label", { children: ["Bursts (", system.emission.bursts.length, ")"] }), jsxRuntime.jsx("button", { className: ParticleEditor_module.addButton, onClick: () => onUpdate({
388
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderDistributionInput('Rate over Time', system.emission.rateOverTime, (v) => onUpdate({ emission: { ...system.emission, rateOverTime: v } })), renderDistributionInput('Rate over Distance', system.emission.rateOverDistance, (v) => onUpdate({ emission: { ...system.emission, rateOverDistance: v } })), jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: t('particleEditor.bursts', 'Bursts ({bursts})').replace('{bursts}', String(system.emission.bursts.length)) }), jsxRuntime.jsx("button", { className: ParticleEditor_module.addButton, onClick: () => onUpdate({
389
389
  emission: {
390
390
  ...system.emission,
391
391
  bursts: [
@@ -546,7 +546,14 @@ function NiceParticleEditor({ systems: initialSystems, onSystemsChange, onPlay,
546
546
  cancelAnimationFrame(animationFrameRef.current);
547
547
  }
548
548
  };
549
- }, [state.isPlaying, state.isPaused, state.playbackSpeed, state.currentTime, selectedSystem]);
549
+ }, [
550
+ state.isPlaying,
551
+ state.isPaused,
552
+ state.playbackSpeed,
553
+ state.currentTime,
554
+ selectedSystem,
555
+ dispatch,
556
+ ]);
550
557
  // Export/Import handlers
551
558
  const handleExport = () => {
552
559
  const json = particleEditorUtils.exportSystemsToJSON(state.systems);
@@ -274,7 +274,7 @@ const NiceCascadedShadows = ({ scene, camera, config: configProp, showDebug: ini
274
274
  setShowDebug(initialDebug);
275
275
  }
276
276
  }, [initialDebug, setShowDebug]);
277
- return (jsxRuntime.jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('cascadedShadows.cascadedShadowMaps', 'Cascaded Shadow Maps')] }), jsxRuntime.jsxs("label", { children: ["Cascades: ", config.cascadeCount, jsxRuntime.jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Max Distance: ", config.maxFar, jsxRuntime.jsx("input", { type: "range", min: 50, max: 1000, step: 10, value: config.maxFar, onChange: (e) => setConfig({ maxFar: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Split Mode:", jsxRuntime.jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "practical", children: t('cascadedShadows.practical', 'Practical') }), jsxRuntime.jsx("option", { value: "logarithmic", children: t('cascadedShadows.logarithmic', 'Logarithmic') }), jsxRuntime.jsx("option", { value: "linear", children: t('cascadedShadows.linear', 'Linear') })] })] }), config.splitMode === 'practical' && (jsxRuntime.jsxs("label", { children: ["Lambda: ", config.practicalSplitLambda.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.practicalSplitLambda, onChange: (e) => setConfig({ practicalSplitLambda: parseFloat(e.target.value) }), style: { width: '100%' } })] })), jsxRuntime.jsxs("label", { children: ["Shadow Map Size: ", config.shadowMapSize, jsxRuntime.jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: 512, children: "512" }), jsxRuntime.jsx("option", { value: 1024, children: "1024" }), jsxRuntime.jsx("option", { value: 2048, children: "2048" }), jsxRuntime.jsx("option", { value: 4096, children: "4096" })] })] }), jsxRuntime.jsxs("div", { style: { fontSize: 10, opacity: 0.7 }, children: ["Cascade splits: ", cascadeSplits.map((s) => s.toFixed(1)).join(' | ')] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), t('cascadedShadows.showCascadeFrustums', 'Show cascade frustums')] })] }));
277
+ return (jsxRuntime.jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('cascadedShadows.cascadedShadowMaps', 'Cascaded Shadow Maps')] }), jsxRuntime.jsxs("label", { children: [t('cascadedShadows.cascades', 'Cascades: {cascadeCount}').replace('{cascadeCount}', String(config.cascadeCount)), jsxRuntime.jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('cascadedShadows.maxDistance', 'Max Distance: {maxFar}').replace('{maxFar}', String(config.maxFar)), jsxRuntime.jsx("input", { type: "range", min: 50, max: 1000, step: 10, value: config.maxFar, onChange: (e) => setConfig({ maxFar: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: [t('cascadedShadows.splitMode', 'Split Mode:'), jsxRuntime.jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "practical", children: t('cascadedShadows.practical', 'Practical') }), jsxRuntime.jsx("option", { value: "logarithmic", children: t('cascadedShadows.logarithmic', 'Logarithmic') }), jsxRuntime.jsx("option", { value: "linear", children: t('cascadedShadows.linear', 'Linear') })] })] }), config.splitMode === 'practical' && (jsxRuntime.jsxs("label", { children: [t('cascadedShadows.lambda', 'Lambda: {practicalSplitLambda}').replace('{practicalSplitLambda}', String(config.practicalSplitLambda.toFixed(2))), jsxRuntime.jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: config.practicalSplitLambda, onChange: (e) => setConfig({ practicalSplitLambda: parseFloat(e.target.value) }), style: { width: '100%' } })] })), jsxRuntime.jsxs("label", { children: [t('cascadedShadows.shadowMapSize', 'Shadow Map Size: {shadowMapSize}').replace('{shadowMapSize}', String(config.shadowMapSize)), jsxRuntime.jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: 512, children: "512" }), jsxRuntime.jsx("option", { value: 1024, children: "1024" }), jsxRuntime.jsx("option", { value: 2048, children: "2048" }), jsxRuntime.jsx("option", { value: 4096, children: "4096" })] })] }), jsxRuntime.jsx("div", { style: { fontSize: 10, opacity: 0.7 }, children: t('cascadedShadows.cascadeSplits', 'Cascade splits: {cascadeSplits}').replace('{cascadeSplits}', String(cascadeSplits.map((s) => s.toFixed(1)).join(' | '))) }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), t('cascadedShadows.showCascadeFrustums', 'Show cascade frustums')] })] }));
278
278
  };
279
279
 
280
280
  exports.NiceCascadedShadows = NiceCascadedShadows;