@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.
- package/CHANGELOG.md +39 -0
- package/dist/cjs/material/NiceMaterialEditor.js +1 -0
- package/dist/cjs/model/ModelEditor.js +7 -1
- package/dist/cjs/model/ModelEditorMenuBar.js +5 -5
- package/dist/cjs/model/ModelEditorRightPanel.js +13 -9
- package/dist/cjs/model/ModelEditorSubComponents.js +9 -3
- package/dist/cjs/model/ModelEditorTimeline.js +3 -1
- package/dist/cjs/model/ModelEditorToolbar.js +1 -1
- package/dist/cjs/model/ModelEditorViewport.js +5 -2
- package/dist/cjs/model/ModelViewer.js +4 -4
- package/dist/cjs/model/NiceArmatureEditor.js +4 -4
- package/dist/cjs/model/NiceMorphTargetEditor.js +3 -1
- package/dist/cjs/model/NiceOctree.js +7 -4
- package/dist/cjs/model/NicePhysicsSimulation.js +4 -4
- package/dist/cjs/model/NiceProceduralGeometry.js +3 -3
- package/dist/cjs/model/NiceTerrainEditor.js +2 -2
- package/dist/cjs/model/NiceWeightPainter.js +2 -2
- package/dist/cjs/model/NiceXRPreview.js +6 -3
- package/dist/cjs/model/useModelEditor.js +92 -73
- package/dist/cjs/model/useModelViewer.js +13 -6
- package/dist/cjs/particle/NiceParticleEditor.js +10 -3
- package/dist/cjs/rendering/NiceCascadedShadows.js +1 -1
- package/dist/cjs/rendering/NiceRenderExport.js +2 -2
- package/dist/cjs/rendering/NiceSSAO.js +2 -1
- package/dist/cjs/rendering/NiceSSR.js +2 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +2 -1
- package/dist/cjs/uv/NiceUVEditor.js +3 -3
- package/dist/esm/material/NiceMaterialEditor.js +1 -0
- package/dist/esm/model/ModelEditor.js +7 -1
- package/dist/esm/model/ModelEditorMenuBar.js +5 -5
- package/dist/esm/model/ModelEditorRightPanel.js +13 -9
- package/dist/esm/model/ModelEditorSubComponents.js +9 -3
- package/dist/esm/model/ModelEditorTimeline.js +3 -1
- package/dist/esm/model/ModelEditorToolbar.js +1 -1
- package/dist/esm/model/ModelEditorViewport.js +5 -2
- package/dist/esm/model/ModelViewer.js +4 -4
- package/dist/esm/model/NiceArmatureEditor.js +4 -4
- package/dist/esm/model/NiceMorphTargetEditor.js +3 -1
- package/dist/esm/model/NiceOctree.js +7 -4
- package/dist/esm/model/NicePhysicsSimulation.js +4 -4
- package/dist/esm/model/NiceProceduralGeometry.js +3 -3
- package/dist/esm/model/NiceTerrainEditor.js +2 -2
- package/dist/esm/model/NiceWeightPainter.js +2 -2
- package/dist/esm/model/NiceXRPreview.js +6 -3
- package/dist/esm/model/useModelEditor.js +92 -73
- package/dist/esm/model/useModelViewer.js +13 -6
- package/dist/esm/particle/NiceParticleEditor.js +10 -3
- package/dist/esm/rendering/NiceCascadedShadows.js +1 -1
- package/dist/esm/rendering/NiceRenderExport.js +2 -2
- package/dist/esm/rendering/NiceSSAO.js +2 -1
- package/dist/esm/rendering/NiceSSR.js +2 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +2 -1
- package/dist/esm/uv/NiceUVEditor.js +3 -3
- package/package.json +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(
|
|
398
|
-
|
|
399
|
+
setStatusText(t('useModelEditor.loadedAnimationS', 'Loaded: {filename} ({clips} animation(s))')
|
|
400
|
+
.replace('{filename}', String(filename))
|
|
401
|
+
.replace('{clips}', String(clips.length)));
|
|
402
|
+
}, [rebuildTree, t]);
|
|
399
403
|
const loadFile = useCallback(async (file) => {
|
|
400
404
|
var _a, _b, _c, _d, _e, _f;
|
|
401
405
|
const name = file.name.toLowerCase();
|
|
402
406
|
const ext = '.' + name.split('.').pop();
|
|
403
407
|
const url = URL.createObjectURL(file);
|
|
404
|
-
setStatusText(
|
|
408
|
+
setStatusText(t('useModelEditor.loading', 'Loading {name}...').replace('{name}', String(file.name)));
|
|
405
409
|
try {
|
|
406
410
|
if (ext === '.fbx') {
|
|
407
411
|
const loader = new FBXLoader();
|
|
@@ -525,32 +529,34 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
525
529
|
addObjectToScene(group, [], file.name);
|
|
526
530
|
}
|
|
527
531
|
else {
|
|
528
|
-
setStatusText(
|
|
532
|
+
setStatusText(t('useModelEditor.unsupportedFormat', 'Unsupported format: {ext}').replace('{ext}', String(ext)));
|
|
529
533
|
}
|
|
530
534
|
}
|
|
531
535
|
catch (err) {
|
|
532
536
|
log.error('Load error:', err);
|
|
533
|
-
setStatusText(
|
|
537
|
+
setStatusText(t('useModelEditor.errorLoading', 'Error loading {name}: {err}')
|
|
538
|
+
.replace('{name}', String(file.name))
|
|
539
|
+
.replace('{err}', String(String(err))));
|
|
534
540
|
}
|
|
535
541
|
finally {
|
|
536
542
|
URL.revokeObjectURL(url);
|
|
537
543
|
}
|
|
538
|
-
}, [addObjectToScene]);
|
|
544
|
+
}, [addObjectToScene, t]);
|
|
539
545
|
const mergeAnimationFBX = useCallback(async (file) => {
|
|
540
546
|
var _a;
|
|
541
547
|
const url = URL.createObjectURL(file);
|
|
542
|
-
setStatusText(
|
|
548
|
+
setStatusText(t('useModelEditor.mergingAnimationsFrom', 'Merging animations from {name}...').replace('{name}', String(file.name)));
|
|
543
549
|
try {
|
|
544
550
|
const loader = new FBXLoader();
|
|
545
551
|
const obj = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
|
|
546
552
|
const clips = (_a = obj.animations) !== null && _a !== void 0 ? _a : [];
|
|
547
553
|
if (clips.length === 0) {
|
|
548
|
-
setStatusText(
|
|
554
|
+
setStatusText(t('useModelEditor.noAnimationsFoundIn', 'No animations found in {name}').replace('{name}', String(file.name)));
|
|
549
555
|
return;
|
|
550
556
|
}
|
|
551
557
|
const root = rootObjectRef.current;
|
|
552
558
|
if (!root) {
|
|
553
|
-
setStatusText('No model loaded — load a base model first');
|
|
559
|
+
setStatusText(t('useModelEditor.noModelLoadedLoadA', 'No model loaded — load a base model first'));
|
|
554
560
|
return;
|
|
555
561
|
}
|
|
556
562
|
if (!mixerRef.current) {
|
|
@@ -564,16 +570,20 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
564
570
|
}));
|
|
565
571
|
allClipsRef.current = [...allClipsRef.current, ...newAnims];
|
|
566
572
|
setAnimations([...allClipsRef.current]);
|
|
567
|
-
setStatusText(
|
|
573
|
+
setStatusText(t('useModelEditor.mergedAnimationSFrom', 'Merged {clips} animation(s) from {name}')
|
|
574
|
+
.replace('{clips}', String(clips.length))
|
|
575
|
+
.replace('{name}', String(file.name)));
|
|
568
576
|
}
|
|
569
577
|
catch (err) {
|
|
570
578
|
log.error('Merge error:', err);
|
|
571
|
-
setStatusText(
|
|
579
|
+
setStatusText(t('useModelEditor.errorMerging', 'Error merging {name}: {err}')
|
|
580
|
+
.replace('{name}', String(file.name))
|
|
581
|
+
.replace('{err}', String(String(err))));
|
|
572
582
|
}
|
|
573
583
|
finally {
|
|
574
584
|
URL.revokeObjectURL(url);
|
|
575
585
|
}
|
|
576
|
-
}, []);
|
|
586
|
+
}, [t]);
|
|
577
587
|
/* ─────────────────────────────────────────
|
|
578
588
|
Animation controls
|
|
579
589
|
───────────────────────────────────────── */
|
|
@@ -931,8 +941,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
931
941
|
clone.position.x += 1;
|
|
932
942
|
sceneRef.current.add(clone);
|
|
933
943
|
rebuildTree();
|
|
934
|
-
setStatusText(
|
|
935
|
-
}, [selectedNode, rebuildTree]);
|
|
944
|
+
setStatusText(t('useModelEditor.duplicated', 'Duplicated {name}').replace('{name}', String(selectedNode.name)));
|
|
945
|
+
}, [selectedNode, rebuildTree, t]);
|
|
936
946
|
/* ─────────────────────────────────────────
|
|
937
947
|
Keyboard shortcuts
|
|
938
948
|
───────────────────────────────────────── */
|
|
@@ -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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1634
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
1735
|
-
|
|
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(
|
|
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(
|
|
441
|
+
setStatusText(t('useModelViewer.loadedTrisMeshes', 'Loaded – {polyCount} tris, {meshCount} meshes')
|
|
442
|
+
.replace('{polyCount}', String(polyCount))
|
|
443
|
+
.replace('{meshCount}', String(meshCount)));
|
|
440
444
|
onLoad === null || onLoad === void 0 ? void 0 : onLoad();
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
catch (err) {
|
|
444
448
|
const msg = err instanceof Error ? err.message : String(err);
|
|
445
449
|
setError(msg);
|
|
446
|
-
setStatusText(
|
|
450
|
+
setStatusText(t('useModelViewer.error', 'Error: {msg}').replace('{msg}', String(msg)));
|
|
447
451
|
onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(msg));
|
|
448
452
|
}
|
|
449
453
|
finally {
|
|
450
454
|
setLoading(false);
|
|
451
455
|
setLoadProgress(1);
|
|
452
456
|
}
|
|
453
|
-
}, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount]);
|
|
457
|
+
}, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount, t]);
|
|
454
458
|
/* ── auto-load from src ── */
|
|
455
459
|
useEffect(() => {
|
|
456
460
|
if (src) {
|
|
@@ -591,9 +595,12 @@ function useModelViewer(props = {}) {
|
|
|
591
595
|
/* ── finalise status text ── */
|
|
592
596
|
useEffect(() => {
|
|
593
597
|
if (!loading && !error && polyCount > 0) {
|
|
594
|
-
setStatusText(
|
|
598
|
+
setStatusText(t('useModelViewer.trianglesMeshesAnimationS', '{polyCount} triangles · {meshCount} meshes · {animNames} animation(s)')
|
|
599
|
+
.replace('{polyCount}', String(polyCount.toLocaleString()))
|
|
600
|
+
.replace('{meshCount}', String(meshCount))
|
|
601
|
+
.replace('{animNames}', String(animNames.length)));
|
|
595
602
|
}
|
|
596
|
-
}, [loading, error, polyCount, meshCount, animNames.length]);
|
|
603
|
+
}, [loading, error, polyCount, meshCount, animNames.length, t]);
|
|
597
604
|
return {
|
|
598
605
|
mountRef,
|
|
599
606
|
loading,
|
|
@@ -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: [
|
|
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: [
|
|
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
|
-
}, [
|
|
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: [
|
|
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: [
|
|
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
|
|
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())] })) })] }));
|