@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
@@ -22,6 +22,7 @@ import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
22
22
  import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader.js';
23
23
  import { VTKLoader } from 'three/examples/jsm/loaders/VTKLoader.js';
24
24
  import { useEditorAsk } from '../core/editorAsk.js';
25
+ import { useNiceTranslation } from '../core/i18n.js';
25
26
  import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
26
27
  import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
27
28
  import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
@@ -55,6 +56,7 @@ function findNodeById(nodes, id) {
55
56
  ═══════════════════════════════════════════ */
56
57
  function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
57
58
  var _a;
59
+ const { t } = useNiceTranslation();
58
60
  const { pendingAsk, ask } = useEditorAsk();
59
61
  /* ── refs ── */
60
62
  const mountRef = useRef(null);
@@ -394,14 +396,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
394
396
  }
395
397
  focusOnObjectRef.current(obj);
396
398
  rebuildTree();
397
- setStatusText(`Loaded: ${filename} (${clips.length} animation(s))`);
398
- }, [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]);
399
403
  const loadFile = useCallback(async (file) => {
400
404
  var _a, _b, _c, _d, _e, _f;
401
405
  const name = file.name.toLowerCase();
402
406
  const ext = '.' + name.split('.').pop();
403
407
  const url = URL.createObjectURL(file);
404
- setStatusText(`Loading ${file.name}...`);
408
+ setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
405
409
  try {
406
410
  if (ext === '.fbx') {
407
411
  const loader = new FBXLoader();
@@ -525,32 +529,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
525
529
  addObjectToScene(group, [], file.name);
526
530
  }
527
531
  else {
528
- setStatusText(`Unsupported format: ${ext}`);
532
+ setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
529
533
  }
530
534
  }
531
535
  catch (err) {
532
536
  log.error('Load error:', err);
533
- setStatusText(`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))));
534
540
  }
535
541
  finally {
536
542
  URL.revokeObjectURL(url);
537
543
  }
538
- }, [addObjectToScene]);
544
+ }, [addObjectToScene, t]);
539
545
  const mergeAnimationFBX = useCallback(async (file) => {
540
546
  var _a;
541
547
  const url = URL.createObjectURL(file);
542
- setStatusText(`Merging animations from ${file.name}...`);
548
+ setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
543
549
  try {
544
550
  const loader = new FBXLoader();
545
551
  const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
546
552
  const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
547
553
  if (clips.length === 0) {
548
- setStatusText(`No animations found in ${file.name}`);
554
+ setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
549
555
  return;
550
556
  }
551
557
  const root = rootObjectRef.current;
552
558
  if (!root) {
553
- setStatusText('No model loaded — load a base model first');
559
+ setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
554
560
  return;
555
561
  }
556
562
  if (!mixerRef.current) {
@@ -564,16 +570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
564
570
  }));
565
571
  allClipsRef.current = [...allClipsRef.current, ...newAnims];
566
572
  setAnimations([...allClipsRef.current]);
567
- setStatusText(`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)));
568
576
  }
569
577
  catch (err) {
570
578
  log.error('Merge error:', err);
571
- 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))));
572
582
  }
573
583
  finally {
574
584
  URL.revokeObjectURL(url);
575
585
  }
576
- }, []);
586
+ }, [t]);
577
587
  /* ─────────────────────────────────────────
578
588
  Animation controls
579
589
  ───────────────────────────────────────── */
@@ -931,8 +941,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
931
941
  clone.position.x += 1;
932
942
  sceneRef.current.add(clone);
933
943
  rebuildTree();
934
- setStatusText(`Duplicated ${selectedNode.name}`);
935
- }, [selectedNode, rebuildTree]);
944
+ setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
945
+ }, [selectedNode, rebuildTree, t]);
936
946
  /* ─────────────────────────────────────────
937
947
  Keyboard shortcuts
938
948
  ───────────────────────────────────────── */
@@ -1076,7 +1086,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1076
1086
  const exportGLTF = useCallback((binary) => {
1077
1087
  const root = rootObjectRef.current;
1078
1088
  if (!root) {
1079
- setStatusText('Nothing to export');
1089
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1080
1090
  return;
1081
1091
  }
1082
1092
  const exporter = new GLTFExporter();
@@ -1097,16 +1107,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1097
1107
  a.download = `export.${binary ? 'glb' : 'gltf'}`;
1098
1108
  a.click();
1099
1109
  URL.revokeObjectURL(a.href);
1100
- setStatusText(`Exported ${binary ? 'GLB' : 'GLTF'} successfully`);
1110
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? 'GLB' : 'GLTF'));
1101
1111
  }, (err) => {
1102
1112
  log.error('Export error:', err);
1103
- setStatusText(`Export error: ${String(err)}`);
1113
+ setStatusText(t('useModelEditor.exportError', 'Export error: {err}').replace('{err}', String(String(err))));
1104
1114
  }, options);
1105
- }, []);
1115
+ }, [t]);
1106
1116
  const exportOBJ = useCallback(() => {
1107
1117
  const root = rootObjectRef.current;
1108
1118
  if (!root) {
1109
- setStatusText('Nothing to export');
1119
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1110
1120
  return;
1111
1121
  }
1112
1122
  const exporter = new OBJExporter();
@@ -1117,12 +1127,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1117
1127
  a.download = 'export.obj';
1118
1128
  a.click();
1119
1129
  URL.revokeObjectURL(a.href);
1120
- setStatusText('Exported OBJ successfully');
1121
- }, []);
1130
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'OBJ'));
1131
+ }, [t]);
1122
1132
  const exportSTL = useCallback((binary) => {
1123
1133
  const root = rootObjectRef.current;
1124
1134
  if (!root) {
1125
- setStatusText('Nothing to export');
1135
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1126
1136
  return;
1127
1137
  }
1128
1138
  const exporter = new STLExporter();
@@ -1139,12 +1149,12 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1139
1149
  a.download = 'export.stl';
1140
1150
  a.click();
1141
1151
  URL.revokeObjectURL(a.href);
1142
- setStatusText(`Exported STL${binary ? ' (binary)' : ''} successfully`);
1143
- }, []);
1152
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.stlBinary', 'STL (binary)') : 'STL'));
1153
+ }, [t]);
1144
1154
  const exportPLY = useCallback((binary) => {
1145
1155
  const root = rootObjectRef.current;
1146
1156
  if (!root) {
1147
- setStatusText('Nothing to export');
1157
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1148
1158
  return;
1149
1159
  }
1150
1160
  const exporter = new PLYExporter();
@@ -1162,13 +1172,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1162
1172
  a.download = 'export.ply';
1163
1173
  a.click();
1164
1174
  URL.revokeObjectURL(a.href);
1165
- setStatusText(`Exported PLY${binary ? ' (binary)' : ''} successfully`);
1175
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', binary ? t('useModelEditor.plyBinary', 'PLY (binary)') : 'PLY'));
1166
1176
  }, { binary });
1167
- }, []);
1177
+ }, [t]);
1168
1178
  const exportUSDZ = useCallback(async () => {
1169
1179
  const root = rootObjectRef.current;
1170
1180
  if (!root) {
1171
- setStatusText('Nothing to export');
1181
+ setStatusText(t('useModelEditor.nothingToExport', 'Nothing to export'));
1172
1182
  return;
1173
1183
  }
1174
1184
  try {
@@ -1183,13 +1193,13 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1183
1193
  a.download = 'export.usdz';
1184
1194
  a.click();
1185
1195
  URL.revokeObjectURL(a.href);
1186
- setStatusText('Exported USDZ successfully');
1196
+ setStatusText(t('useModelEditor.exportedFormat', 'Exported {format} successfully').replace('{format}', 'USDZ'));
1187
1197
  }
1188
1198
  catch (err) {
1189
1199
  log.error('USDZ export error:', err);
1190
- setStatusText(`USDZ export error: ${String(err)}`);
1200
+ setStatusText(t('useModelEditor.usdzExportError', 'USDZ export error: {err}').replace('{err}', String(String(err))));
1191
1201
  }
1192
- }, []);
1202
+ }, [t]);
1193
1203
  /* ─────────────────────────────────────────
1194
1204
  Dispose & clear
1195
1205
  ───────────────────────────────────────── */
@@ -1258,8 +1268,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1258
1268
  setAnimDuration(0);
1259
1269
  setSelectedId(null);
1260
1270
  rebuildTree();
1261
- setStatusText('Scene cleared');
1262
- }, [rebuildTree, disposeObject]);
1271
+ setStatusText(t('useModelEditor.sceneCleared', 'Scene cleared'));
1272
+ }, [rebuildTree, disposeObject, t]);
1263
1273
  // asks first: clearing cannot be undone
1264
1274
  const clearScene = useCallback(() => {
1265
1275
  void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
@@ -1277,7 +1287,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1277
1287
  }
1278
1288
  const root = rootObjectRef.current;
1279
1289
  if (!root) {
1280
- setStatusText('Nothing to save');
1290
+ setStatusText(t('useModelEditor.nothingToSave', 'Nothing to save'));
1281
1291
  return;
1282
1292
  }
1283
1293
  const exporter = new GLTFExporter();
@@ -1295,9 +1305,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1295
1305
  reader.readAsDataURL(blob);
1296
1306
  }, (err) => {
1297
1307
  log.error('Save to library error:', err);
1298
- setStatusText(`Save error: ${String(err)}`);
1308
+ setStatusText(t('useModelEditor.saveError', 'Save error: {err}').replace('{err}', String(String(err))));
1299
1309
  }, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
1300
- }, [onSaveToLibrary, ask]);
1310
+ }, [onSaveToLibrary, ask, t]);
1301
1311
  /* ─────────────────────────────────────────
1302
1312
  Add Mesh / Light / Camera / Empty
1303
1313
  ───────────────────────────────────────── */
@@ -1364,9 +1374,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1364
1374
  }
1365
1375
  sceneRef.current.add(mesh);
1366
1376
  rebuildTree();
1367
- setStatusText(`Added ${mesh.name}`);
1377
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(mesh.name)));
1368
1378
  setAddMenuOpen(null);
1369
- }, [rebuildTree]);
1379
+ }, [rebuildTree, t]);
1370
1380
  const addSceneLight = useCallback((type) => {
1371
1381
  let light;
1372
1382
  switch (type) {
@@ -1418,9 +1428,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1418
1428
  sceneRef.current.add(helper);
1419
1429
  }
1420
1430
  rebuildTree();
1421
- setStatusText(`Added ${light.name}`);
1431
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(light.name)));
1422
1432
  setAddMenuOpen(null);
1423
- }, [rebuildTree]);
1433
+ }, [rebuildTree, t]);
1424
1434
  const addCameraObject = useCallback(() => {
1425
1435
  const cam = new THREE.PerspectiveCamera(50, 16 / 9, 0.1, 1000);
1426
1436
  cam.name = 'Camera';
@@ -1431,9 +1441,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1431
1441
  helper.name = '__helper_Camera';
1432
1442
  sceneRef.current.add(helper);
1433
1443
  rebuildTree();
1434
- setStatusText('Added Camera');
1444
+ setStatusText(t('useModelEditor.addedCamera', 'Added Camera'));
1435
1445
  setAddMenuOpen(null);
1436
- }, [rebuildTree]);
1446
+ }, [rebuildTree, t]);
1437
1447
  const addEmpty = useCallback((type) => {
1438
1448
  const obj = new THREE.Object3D();
1439
1449
  obj.name =
@@ -1445,9 +1455,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1445
1455
  obj.add(h);
1446
1456
  }
1447
1457
  rebuildTree();
1448
- setStatusText(`Added ${obj.name}`);
1458
+ setStatusText(t('useModelEditor.added', 'Added {name}').replace('{name}', String(obj.name)));
1449
1459
  setAddMenuOpen(null);
1450
- }, [rebuildTree]);
1460
+ }, [rebuildTree, t]);
1451
1461
  /* ─────────────────────────────────────────
1452
1462
  Raycaster click-to-select
1453
1463
  ───────────────────────────────────────── */
@@ -1515,14 +1525,16 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1515
1525
  }
1516
1526
  e.target.value = '';
1517
1527
  if (!onAIPoseVideo) {
1518
- setAiStatus('AI pose callback not configured.');
1528
+ setAiStatus(t('useModelEditor.aiPoseCallbackNotConfigured', 'AI pose callback not configured.'));
1519
1529
  return;
1520
1530
  }
1521
1531
  setAiBusy(true);
1522
- setAiStatus('Uploading video for AI pose estimation...');
1532
+ setAiStatus(t('useModelEditor.uploadingVideoForAiPose', 'Uploading video for AI pose estimation...'));
1523
1533
  try {
1524
1534
  const result = await onAIPoseVideo(file);
1525
- 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)));
1526
1538
  const clip = pose3dToClip(result);
1527
1539
  if (clip && rootObjectRef.current) {
1528
1540
  if (!mixerRef.current) {
@@ -1537,20 +1549,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1537
1549
  allClipsRef.current = [...allClipsRef.current, newAnim];
1538
1550
  setAnimations([...allClipsRef.current]);
1539
1551
  playClip(allClipsRef.current.length - 1);
1540
- setAiStatus('Animation created successfully!');
1552
+ setAiStatus(t('useModelEditor.animationCreatedSuccessfully', 'Animation created successfully!'));
1541
1553
  }
1542
1554
  else {
1543
- setAiStatus('Could not create animation — load a rigged model first.');
1555
+ setAiStatus(t('useModelEditor.couldNotCreateAnimationLoad', 'Could not create animation — load a rigged model first.'));
1544
1556
  }
1545
1557
  }
1546
1558
  catch (err) {
1547
1559
  log.error('AI pose error:', err);
1548
- setAiStatus(`Error: ${String(err)}`);
1560
+ setAiStatus(t('useModelEditor.error', 'Error: {err}').replace('{err}', String(String(err))));
1549
1561
  }
1550
1562
  finally {
1551
1563
  setAiBusy(false);
1552
1564
  }
1553
- }, [playClip, onAIPoseVideo]);
1565
+ }, [playClip, onAIPoseVideo, t]);
1554
1566
  /* ─────────────────────────────────────────
1555
1567
  Node helpers
1556
1568
  ───────────────────────────────────────── */
@@ -1574,19 +1586,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1574
1586
  const undo = useCallback(() => {
1575
1587
  const label = undoRedoRef.current.undo();
1576
1588
  if (label) {
1577
- setStatusText(`Undo: ${label}`);
1589
+ setStatusText(t('useModelEditor.undo', 'Undo: {label}').replace('{label}', String(label)));
1578
1590
  rebuildTree();
1579
1591
  }
1580
1592
  setUndoRedoVer((v) => v + 1);
1581
- }, [rebuildTree]);
1593
+ }, [rebuildTree, t]);
1582
1594
  const redo = useCallback(() => {
1583
1595
  const label = undoRedoRef.current.redo();
1584
1596
  if (label) {
1585
- setStatusText(`Redo: ${label}`);
1597
+ setStatusText(t('useModelEditor.redo', 'Redo: {label}').replace('{label}', String(label)));
1586
1598
  rebuildTree();
1587
1599
  }
1588
1600
  setUndoRedoVer((v) => v + 1);
1589
- }, [rebuildTree]);
1601
+ }, [rebuildTree, t]);
1590
1602
  const canUndo = undoRedoRef.current.canUndo();
1591
1603
  const canRedo = undoRedoRef.current.canRedo();
1592
1604
  const undoLabel = undoRedoRef.current.undoLabel;
@@ -1615,30 +1627,35 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1615
1627
  return SUPPORTED_EXTENSIONS.includes(ext);
1616
1628
  });
1617
1629
  if (fileArray.length === 0) {
1618
- setStatusText('No supported files found');
1630
+ setStatusText(t('useModelEditor.noSupportedFilesFound', 'No supported files found'));
1619
1631
  return;
1620
1632
  }
1621
- setStatusText(`Importing ${fileArray.length} file(s)...`);
1633
+ setStatusText(t('useModelEditor.importingFileS', 'Importing {fileArray} file(s)...').replace('{fileArray}', String(fileArray.length)));
1622
1634
  let loaded = 0;
1623
1635
  for (const file of fileArray) {
1624
1636
  try {
1625
1637
  await loadFile(file);
1626
1638
  loaded++;
1627
- 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)));
1628
1643
  }
1629
1644
  catch (err) {
1630
1645
  log.error(`Failed to import ${file.name}:`, err);
1631
1646
  }
1632
1647
  }
1633
- setStatusText(`Batch import complete: ${loaded}/${fileArray.length} files loaded`);
1634
- }, [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]);
1635
1652
  /* ─────────────────────────────────────────
1636
1653
  PBR Presets
1637
1654
  ───────────────────────────────────────── */
1638
1655
  const applyPreset = useCallback((preset) => {
1639
1656
  const node = selectedNode;
1640
1657
  if (!node || !(node.object instanceof THREE.Mesh)) {
1641
- setStatusText('Select a mesh to apply material preset');
1658
+ setStatusText(t('useModelEditor.selectAMeshToApply', 'Select a mesh to apply material preset'));
1642
1659
  return;
1643
1660
  }
1644
1661
  const mesh = node.object;
@@ -1671,7 +1688,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1671
1688
  mesh.material = newMaterial;
1672
1689
  }
1673
1690
  pushUndo({
1674
- label: `Apply ${preset.name} preset`,
1691
+ label: t('useModelEditor.applyPreset', 'Apply {name} preset').replace('{name}', String(preset.name)),
1675
1692
  undo: () => {
1676
1693
  if (Array.isArray(mesh.material)) {
1677
1694
  mesh.material[materialIdx] = oldClone;
@@ -1690,15 +1707,15 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1690
1707
  },
1691
1708
  });
1692
1709
  setMatRefresh((v) => v + 1);
1693
- setStatusText(`Applied ${preset.name} preset`);
1694
- }, [selectedNode, selMaterialIdx, pushUndo]);
1710
+ setStatusText(t('useModelEditor.appliedPreset', 'Applied {name} preset').replace('{name}', String(preset.name)));
1711
+ }, [selectedNode, selMaterialIdx, pushUndo, t]);
1695
1712
  /* ─────────────────────────────────────────
1696
1713
  LOD Generation
1697
1714
  ───────────────────────────────────────── */
1698
1715
  const generateSelectedLOD = useCallback(() => {
1699
1716
  const node = selectedNode;
1700
1717
  if (!node || !(node.object instanceof THREE.Mesh)) {
1701
- setStatusText('Select a mesh to generate LOD');
1718
+ setStatusText(t('useModelEditor.selectAMeshToGenerate', 'Select a mesh to generate LOD'));
1702
1719
  return;
1703
1720
  }
1704
1721
  const mesh = node.object;
@@ -1707,7 +1724,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1707
1724
  parent.remove(mesh);
1708
1725
  parent.add(lod);
1709
1726
  pushUndo({
1710
- label: `Generate LOD for ${mesh.name}`,
1727
+ label: t('useModelEditor.generateLodFor', 'Generate LOD for {name}').replace('{name}', String(mesh.name)),
1711
1728
  undo: () => {
1712
1729
  parent.remove(lod);
1713
1730
  parent.add(mesh);
@@ -1718,8 +1735,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1718
1735
  },
1719
1736
  });
1720
1737
  rebuildTree();
1721
- setStatusText(`Generated LOD for ${mesh.name} (4 levels)`);
1722
- }, [selectedNode, pushUndo, rebuildTree]);
1738
+ setStatusText(t('useModelEditor.generatedLodFor4Levels', 'Generated LOD for {name} (4 levels)').replace('{name}', String(mesh.name)));
1739
+ }, [selectedNode, pushUndo, rebuildTree, t]);
1723
1740
  /* ─────────────────────────────────────────
1724
1741
  Instancing
1725
1742
  ───────────────────────────────────────── */
@@ -1727,12 +1744,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1727
1744
  const root = rootObjectRef.current || sceneRef.current;
1728
1745
  const result = convertToInstanced(root);
1729
1746
  if (result.created === 0) {
1730
- setStatusText('No duplicate meshes found to instance');
1747
+ setStatusText(t('useModelEditor.noDuplicateMeshesFoundTo', 'No duplicate meshes found to instance'));
1731
1748
  return;
1732
1749
  }
1733
1750
  rebuildTree();
1734
- setStatusText(`Created ${result.created} instanced meshes, removed ${result.removed} duplicates`);
1735
- }, [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]);
1736
1755
  /* ── HDRI Environment ── */
1737
1756
  const applyHDRI = useCallback((preset) => {
1738
1757
  const renderer = rendererRef.current;
@@ -1750,8 +1769,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
1750
1769
  else {
1751
1770
  setFogEnabled(false);
1752
1771
  }
1753
- setStatusText(`Environment: ${preset.name}`);
1754
- }, []);
1772
+ setStatusText(t('useModelEditor.environment', 'Environment: {name}').replace('{name}', String(preset.name)));
1773
+ }, [t]);
1755
1774
  /* ── Snap ── */
1756
1775
  const snapPoint = useCallback((point) => {
1757
1776
  const root = rootObjectRef.current || sceneRef.current;
@@ -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,
@@ -295,7 +295,7 @@ function SystemListPanel({ systems, selectedId, onSelect, onAdd, onRemove, onDup
295
295
  }
296
296
  setEditingId(null);
297
297
  };
298
- return (jsxs("div", { className: styles.systemList, children: [jsxs("div", { className: styles.systemListHeader, children: [jsxs("span", { children: ["Systems (", systems.length, ")"] }), jsxs("div", { className: styles.systemListActions, children: [jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsx(NiceIcon, { name: "plus", size: 14 }) }), jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsx(NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsx("div", { className: styles.presetMenu, children: Object.keys(PARTICLE_PRESETS).map((preset) => (jsx("button", { className: styles.presetItem, onClick: () => {
298
+ return (jsxs("div", { className: styles.systemList, children: [jsxs("div", { className: styles.systemListHeader, children: [jsx("span", { children: t('particleEditor.systems', 'Systems ({systems})').replace('{systems}', String(systems.length)) }), jsxs("div", { className: styles.systemListActions, children: [jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsx(NiceIcon, { name: "plus", size: 14 }) }), jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsx(NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsx("div", { className: styles.presetMenu, children: Object.keys(PARTICLE_PRESETS).map((preset) => (jsx("button", { className: styles.presetItem, onClick: () => {
299
299
  onAddPreset(preset);
300
300
  setShowPresets(false);
301
301
  }, children: preset.charAt(0).toUpperCase() + preset.slice(1) }, preset))) })), jsx("div", { className: styles.systemItems, children: systems.map((system) => (jsx("div", { className: `${styles.systemItem} ${selectedId === system.id ? styles.selected : ''}`, onClick: () => onSelect(system.id), children: editingId === system.id ? (jsx("input", { type: "text", value: editName, onChange: (e) => setEditName(e.target.value), onBlur: handleFinishRename, onKeyDown: (e) => e.key === 'Enter' && handleFinishRename(), autoFocus: true, className: styles.renameInput })) : (jsxs(Fragment, { children: [jsx("span", { className: styles.systemName, onDoubleClick: () => handleStartRename(system), children: system.name }), jsxs("div", { className: styles.systemItemActions, children: [jsx("button", { onClick: (e) => {
@@ -383,7 +383,7 @@ function ModuleEditor({ system, moduleKey, onUpdate }) {
383
383
  step: 0.1,
384
384
  }), 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 })] }));
385
385
  case 'emission':
386
- return (jsxs(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 } })), jsxs("div", { className: styles.propertyRow, children: [jsxs("label", { children: ["Bursts (", system.emission.bursts.length, ")"] }), jsx("button", { className: styles.addButton, onClick: () => onUpdate({
386
+ return (jsxs(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 } })), jsxs("div", { className: styles.propertyRow, children: [jsx("label", { children: t('particleEditor.bursts', 'Bursts ({bursts})').replace('{bursts}', String(system.emission.bursts.length)) }), jsx("button", { className: styles.addButton, onClick: () => onUpdate({
387
387
  emission: {
388
388
  ...system.emission,
389
389
  bursts: [
@@ -544,7 +544,14 @@ function NiceParticleEditor({ systems: initialSystems, onSystemsChange, onPlay,
544
544
  cancelAnimationFrame(animationFrameRef.current);
545
545
  }
546
546
  };
547
- }, [state.isPlaying, state.isPaused, state.playbackSpeed, state.currentTime, selectedSystem]);
547
+ }, [
548
+ state.isPlaying,
549
+ state.isPaused,
550
+ state.playbackSpeed,
551
+ state.currentTime,
552
+ selectedSystem,
553
+ dispatch,
554
+ ]);
548
555
  // Export/Import handlers
549
556
  const handleExport = () => {
550
557
  const json = exportSystemsToJSON(state.systems);
@@ -253,7 +253,7 @@ const NiceCascadedShadows = ({ scene, camera, config: configProp, showDebug: ini
253
253
  setShowDebug(initialDebug);
254
254
  }
255
255
  }, [initialDebug, setShowDebug]);
256
- return (jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('cascadedShadows.cascadedShadowMaps', 'Cascaded Shadow Maps')] }), jsxs("label", { children: ["Cascades: ", config.cascadeCount, jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Max Distance: ", config.maxFar, 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%' } })] }), jsxs("label", { children: ["Split Mode:", jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "practical", children: t('cascadedShadows.practical', 'Practical') }), jsx("option", { value: "logarithmic", children: t('cascadedShadows.logarithmic', 'Logarithmic') }), jsx("option", { value: "linear", children: t('cascadedShadows.linear', 'Linear') })] })] }), config.splitMode === 'practical' && (jsxs("label", { children: ["Lambda: ", config.practicalSplitLambda.toFixed(2), 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%' } })] })), jsxs("label", { children: ["Shadow Map Size: ", config.shadowMapSize, jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 512, children: "512" }), jsx("option", { value: 1024, children: "1024" }), jsx("option", { value: 2048, children: "2048" }), jsx("option", { value: 4096, children: "4096" })] })] }), jsxs("div", { style: { fontSize: 10, opacity: 0.7 }, children: ["Cascade splits: ", cascadeSplits.map((s) => s.toFixed(1)).join(' | ')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), t('cascadedShadows.showCascadeFrustums', 'Show cascade frustums')] })] }));
256
+ return (jsxs("div", { className: `nice-cascaded-shadows ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "lightbulb", size: 14 }), ' ', t('cascadedShadows.cascadedShadowMaps', 'Cascaded Shadow Maps')] }), jsxs("label", { children: [t('cascadedShadows.cascades', 'Cascades: {cascadeCount}').replace('{cascadeCount}', String(config.cascadeCount)), jsx("input", { type: "range", min: 1, max: 5, value: config.cascadeCount, onChange: (e) => setConfig({ cascadeCount: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('cascadedShadows.maxDistance', 'Max Distance: {maxFar}').replace('{maxFar}', String(config.maxFar)), 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%' } })] }), jsxs("label", { children: [t('cascadedShadows.splitMode', 'Split Mode:'), jsxs("select", { value: config.splitMode, onChange: (e) => setConfig({ splitMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "practical", children: t('cascadedShadows.practical', 'Practical') }), jsx("option", { value: "logarithmic", children: t('cascadedShadows.logarithmic', 'Logarithmic') }), jsx("option", { value: "linear", children: t('cascadedShadows.linear', 'Linear') })] })] }), config.splitMode === 'practical' && (jsxs("label", { children: [t('cascadedShadows.lambda', 'Lambda: {practicalSplitLambda}').replace('{practicalSplitLambda}', String(config.practicalSplitLambda.toFixed(2))), 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%' } })] })), jsxs("label", { children: [t('cascadedShadows.shadowMapSize', 'Shadow Map Size: {shadowMapSize}').replace('{shadowMapSize}', String(config.shadowMapSize)), jsxs("select", { value: config.shadowMapSize, onChange: (e) => setConfig({ shadowMapSize: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 512, children: "512" }), jsx("option", { value: 1024, children: "1024" }), jsx("option", { value: 2048, children: "2048" }), jsx("option", { value: 4096, children: "4096" })] })] }), 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(' | '))) }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showDebug, onChange: (e) => setShowDebug(e.target.checked) }), t('cascadedShadows.showCascadeFrustums', 'Show cascade frustums')] })] }));
257
257
  };
258
258
 
259
259
  export { NiceCascadedShadows, useCSM };
@@ -325,7 +325,7 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
325
325
  color: config.width === p.width && config.height === p.height
326
326
  ? 'var(--nice-color-white, #fff)'
327
327
  : 'inherit',
328
- }, children: p.label }, p.label))) }), jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsx("span", { children: "\u00D7" }), jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxs("label", { children: ["Format:", jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "png", children: "PNG (lossless + alpha)" }), jsx("option", { value: "jpeg", children: "JPEG (lossy, no alpha)" }), jsx("option", { value: "exr", children: "EXR (HDR float)" })] })] }), jsxs("label", { children: ["Background:", jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: ["Color:", jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxs("label", { children: ["Supersampling: ", config.samples, "x", jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 1, children: "1x (none)" }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: "4x (best quality)" })] })] }), jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
328
+ }, children: p.label }, p.label))) }), jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsx("span", { children: "\u00D7" }), jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxs("label", { children: [t('renderExport.format', 'Format:'), jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "png", children: t('renderExport.pngLosslessAlpha', 'PNG (lossless + alpha)') }), jsx("option", { value: "jpeg", children: t('renderExport.jpegLossyNoAlpha', 'JPEG (lossy, no alpha)') }), jsx("option", { value: "exr", children: t('renderExport.exrHdrFloat', 'EXR (HDR float)') })] })] }), jsxs("label", { children: [t('renderExport.background', 'Background:'), jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [t('renderExport.color', 'Color:'), jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxs("label", { children: [t('renderExport.supersamplingX', 'Supersampling: {samples}x').replace('{samples}', String(config.samples)), jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 1, children: t('renderExport.n1xNone', '1x (none)') }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: t('renderExport.n4xBestQuality', '4x (best quality)') })] })] }), jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
329
329
  background: isRendering
330
330
  ? 'var(--nice-bg-tertiary, #94a3b8)'
331
331
  : 'var(--nice-color-accent, #6366f1)',
@@ -334,7 +334,7 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
334
334
  borderRadius: 4,
335
335
  padding: '8px 12px',
336
336
  cursor: isRendering ? 'wait' : 'pointer',
337
- }, children: isRendering ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "clock", size: 14 }), " Rendering\u2026"] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
337
+ }, children: isRendering ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "clock", size: 14 }), " ", t('renderExport.rendering', 'Rendering…')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
338
338
  .replace('{width}', String(config.width))
339
339
  .replace('{height}', String(config.height))
340
340
  .replace('{format}', config.format.toUpperCase())] })) })] }));