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