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