@nice2dev/ui-3d 1.2.0 → 1.2.1
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 +103 -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 +7 -3
- package/dist/cjs/material/materialEditorTypes.js +1 -1
- package/dist/cjs/material/materialNodeDefinitions.js +14 -11
- package/dist/cjs/model/ModelEditor.js +3 -2
- package/dist/cjs/model/ModelEditorLeftPanel.js +10 -4
- package/dist/cjs/model/ModelEditorMenuBar.js +7 -5
- package/dist/cjs/model/ModelEditorRightPanel.js +18 -16
- package/dist/cjs/model/ModelEditorSubComponents.js +8 -6
- package/dist/cjs/model/ModelEditorTimeline.js +6 -4
- package/dist/cjs/model/ModelEditorToolbar.js +6 -4
- package/dist/cjs/model/ModelEditorViewport.js +16 -6
- package/dist/cjs/model/ModelViewer.js +6 -4
- package/dist/cjs/model/NiceArmatureEditor.js +6 -4
- package/dist/cjs/model/NiceMorphTargetEditor.js +5 -3
- package/dist/cjs/model/NiceOctree.js +3 -1
- package/dist/cjs/model/NicePhysicsSimulation.js +6 -4
- package/dist/cjs/model/NiceProceduralGeometry.js +5 -3
- package/dist/cjs/model/NiceTerrainEditor.js +8 -6
- package/dist/cjs/model/NiceWeightPainter.js +4 -2
- package/dist/cjs/model/NiceXRPreview.js +4 -2
- package/dist/cjs/model/useModelEditor.js +22 -15
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +14 -9
- package/dist/cjs/rendering/NiceCascadedShadows.js +3 -1
- package/dist/cjs/rendering/NiceRenderExport.js +9 -3
- package/dist/cjs/rendering/NiceSSAO.js +3 -1
- package/dist/cjs/rendering/NiceSSR.js +3 -1
- package/dist/cjs/rendering/NiceWebGPURenderer.js +4 -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 +7 -3
- package/dist/esm/material/materialEditorTypes.js +1 -1
- package/dist/esm/material/materialNodeDefinitions.js +14 -11
- package/dist/esm/model/ModelEditor.js +3 -2
- package/dist/esm/model/ModelEditorLeftPanel.js +10 -4
- package/dist/esm/model/ModelEditorMenuBar.js +7 -5
- package/dist/esm/model/ModelEditorRightPanel.js +18 -16
- package/dist/esm/model/ModelEditorSubComponents.js +8 -6
- package/dist/esm/model/ModelEditorTimeline.js +6 -4
- package/dist/esm/model/ModelEditorToolbar.js +6 -4
- package/dist/esm/model/ModelEditorViewport.js +16 -6
- package/dist/esm/model/ModelViewer.js +6 -4
- package/dist/esm/model/NiceArmatureEditor.js +6 -4
- package/dist/esm/model/NiceMorphTargetEditor.js +5 -3
- package/dist/esm/model/NiceOctree.js +3 -1
- package/dist/esm/model/NicePhysicsSimulation.js +6 -4
- package/dist/esm/model/NiceProceduralGeometry.js +5 -3
- package/dist/esm/model/NiceTerrainEditor.js +8 -6
- package/dist/esm/model/NiceWeightPainter.js +4 -2
- package/dist/esm/model/NiceXRPreview.js +4 -2
- package/dist/esm/model/useModelEditor.js +22 -15
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +14 -9
- package/dist/esm/rendering/NiceCascadedShadows.js +3 -1
- package/dist/esm/rendering/NiceRenderExport.js +9 -3
- package/dist/esm/rendering/NiceSSAO.js +3 -1
- package/dist/esm/rendering/NiceSSR.js +3 -1
- package/dist/esm/rendering/NiceWebGPURenderer.js +4 -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
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/* ── Octree implementation ── */
|
|
7
8
|
const DEFAULT_CONFIG = {
|
|
@@ -324,6 +325,7 @@ function useOctree(scene, configOverride) {
|
|
|
324
325
|
}
|
|
325
326
|
/* ── Component ── */
|
|
326
327
|
const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = '', }) => {
|
|
328
|
+
const { t } = useNiceTranslation();
|
|
327
329
|
const octree = useOctree(scene, config);
|
|
328
330
|
useEffect(() => {
|
|
329
331
|
if (initialShowDebug !== undefined) {
|
|
@@ -337,7 +339,7 @@ const NiceOctree = ({ scene, config, showDebug: initialShowDebug, className = ''
|
|
|
337
339
|
borderRadius: 4,
|
|
338
340
|
padding: '6px 12px',
|
|
339
341
|
cursor: 'pointer',
|
|
340
|
-
}, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Build Octree
|
|
342
|
+
}, children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('octree.buildOctree', 'Build Octree')] }), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Nodes: ", octree.nodeCount, " | Objects: ", octree.objectCount] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: octree.showDebug, onChange: (e) => octree.setShowDebug(e.target.checked) }), t('octree.showDebugWireframes', 'Show debug wireframes')] })] }));
|
|
341
343
|
};
|
|
342
344
|
|
|
343
345
|
export { NiceOctree, useOctree };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
function inferColliderShape(object) {
|
|
7
8
|
if (object instanceof THREE.Mesh) {
|
|
@@ -261,11 +262,12 @@ function usePhysicsSimulation(scene, onSimulationStep) {
|
|
|
261
262
|
}
|
|
262
263
|
/* ── Component ── */
|
|
263
264
|
const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, className = '', }) => {
|
|
265
|
+
const { t } = useNiceTranslation();
|
|
264
266
|
const { worldConfig, setWorldConfig, bodies, addBody, removeBody, updateBody, getBodyForObject, isRunning, play, pause, step, reset, debugDraw, setDebugDraw, } = usePhysicsSimulation(scene, onSimulationStep);
|
|
265
267
|
const selectedBody = selectedObject
|
|
266
268
|
? getBodyForObject(selectedObject.name || selectedObject.uuid)
|
|
267
269
|
: undefined;
|
|
268
|
-
return (jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "zap", size: 14 }), " Physics
|
|
270
|
+
return (jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "zap", size: 14 }), " ", t('physicsSimulation.physics', 'Physics')] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), t('physicsSimulation.debug', 'Debug')] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
|
|
269
271
|
flex: 1,
|
|
270
272
|
background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
|
|
271
273
|
color: 'var(--nice-color-white, #fff)',
|
|
@@ -273,7 +275,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
|
|
|
273
275
|
borderRadius: 4,
|
|
274
276
|
padding: '5px 8px',
|
|
275
277
|
cursor: 'pointer',
|
|
276
|
-
}, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " Pause
|
|
278
|
+
}, children: isRunning ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "pause", size: 14 }), " ", t('physicsSimulation.pause', 'Pause')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('physicsSimulation.play', 'Play')] })) }), jsxs("button", { type: "button", onClick: step, disabled: isRunning, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "skip-forward", size: 14 }), " ", t('physicsSimulation.step', 'Step')] }), jsxs("button", { type: "button", onClick: reset, style: { padding: '5px 8px', cursor: 'pointer' }, children: [jsx(NiceIcon, { name: "refresh", size: 14 }), " ", t('physicsSimulation.reset', 'Reset')] })] }), jsxs("label", { children: ["Gravity Y: ", worldConfig.gravity.y.toFixed(1), jsx("input", { type: "range", min: -20, max: 0, step: 0.1, value: worldConfig.gravity.y, onChange: (e) => setWorldConfig({ gravity: new THREE.Vector3(0, parseFloat(e.target.value), 0) }), style: { width: '100%' } })] }), selectedObject && (jsxs("fieldset", { style: {
|
|
277
279
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
278
280
|
borderRadius: 6,
|
|
279
281
|
padding: 6,
|
|
@@ -286,7 +288,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
|
|
|
286
288
|
borderRadius: 4,
|
|
287
289
|
padding: '5px 8px',
|
|
288
290
|
cursor: 'pointer',
|
|
289
|
-
}, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children:
|
|
291
|
+
}, children: "+ Add Rigid Body" })) : (jsxs(Fragment, { children: [jsxs("label", { children: ["Type:", jsxs("select", { value: selectedBody.bodyType, onChange: (e) => updateBody(selectedBody.id, { bodyType: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "dynamic", children: t('physicsSimulation.dynamic', 'Dynamic') }), jsx("option", { value: "static", children: t('physicsSimulation.static', 'Static') }), jsx("option", { value: "kinematic", children: t('physicsSimulation.kinematic', 'Kinematic') })] })] }), jsxs("label", { children: ["Shape:", jsxs("select", { value: selectedBody.colliderShape, onChange: (e) => updateBody(selectedBody.id, { colliderShape: e.target.value }), style: { marginLeft: 8, width: '100%' }, children: [jsx("option", { value: "box", children: "Box" }), jsx("option", { value: "sphere", children: t('physicsSimulation.sphere', 'Sphere') }), jsx("option", { value: "capsule", children: t('physicsSimulation.capsule', 'Capsule') }), jsx("option", { value: "convexHull", children: t('physicsSimulation.convexHull', 'Convex Hull') }), jsx("option", { value: "trimesh", children: t('physicsSimulation.trimesh', 'Trimesh') })] })] }), jsxs("label", { children: ["Mass: ", selectedBody.mass.toFixed(1), jsx("input", { type: "range", min: 0.1, max: 100, step: 0.1, value: selectedBody.mass, onChange: (e) => updateBody(selectedBody.id, { mass: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Restitution: ", selectedBody.restitution.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.restitution, onChange: (e) => updateBody(selectedBody.id, { restitution: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Friction: ", selectedBody.friction.toFixed(2), jsx("input", { type: "range", min: 0, max: 1, step: 0.01, value: selectedBody.friction, onChange: (e) => updateBody(selectedBody.id, { friction: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: selectedBody.isSensor, onChange: (e) => updateBody(selectedBody.id, { isSensor: e.target.checked }) }), "Sensor (trigger only)"] }), jsx("button", { type: "button", onClick: () => removeBody(selectedBody.id), style: {
|
|
290
292
|
background: 'var(--nice-danger, #ef4444)',
|
|
291
293
|
color: 'var(--nice-text-on-primary, #fff)',
|
|
292
294
|
border: 'none',
|
|
@@ -294,7 +296,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
|
|
|
294
296
|
padding: '4px 8px',
|
|
295
297
|
cursor: 'pointer',
|
|
296
298
|
fontSize: 11,
|
|
297
|
-
}, children:
|
|
299
|
+
}, children: t('physicsSimulation.removeRigidBody', 'Remove Rigid Body') })] }))] })), jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Total bodies: ", bodies.length] })] }));
|
|
298
300
|
};
|
|
299
301
|
|
|
300
302
|
export { NicePhysicsSimulation, usePhysicsSimulation };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/* ── Generators ── */
|
|
7
8
|
function seededRng(seed) {
|
|
@@ -292,21 +293,22 @@ function useProceduralGeometry(scene, onGenerated) {
|
|
|
292
293
|
}
|
|
293
294
|
/* ── Component ── */
|
|
294
295
|
const NiceProceduralGeometry = ({ scene, onGenerated, className = '', }) => {
|
|
296
|
+
const { t } = useNiceTranslation();
|
|
295
297
|
const { type, setType, treeParams, setTreeParams, rockParams, setRockParams, buildingParams, setBuildingParams, gearParams, setGearParams, generate, } = useProceduralGeometry(scene, onGenerated);
|
|
296
|
-
return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children:
|
|
298
|
+
return (jsxs("div", { className: `nice-procedural-geometry ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("strong", { children: t('proceduralGeometry.proceduralGeometry', 'Procedural Geometry') }), jsxs("select", { value: type, onChange: (e) => setType(e.target.value), style: { width: '100%', padding: '4px 8px' }, children: [jsx("option", { value: "tree", children: t('proceduralGeometry.tree', 'Tree') }), jsx("option", { value: "rock", children: t('proceduralGeometry.rock', 'Rock') }), jsx("option", { value: "building", children: t('proceduralGeometry.building', 'Building') }), jsx("option", { value: "pipe", children: t('proceduralGeometry.pipe', 'Pipe') }), jsx("option", { value: "gear", children: t('proceduralGeometry.gear', 'Gear') })] }), type === 'tree' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Height: ", treeParams.trunkHeight.toFixed(1), jsx("input", { type: "range", min: 1, max: 15, step: 0.5, value: treeParams.trunkHeight, onChange: (e) => setTreeParams({ ...treeParams, trunkHeight: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Branches: ", treeParams.branches, jsx("input", { type: "range", min: 1, max: 6, value: treeParams.branches, onChange: (e) => setTreeParams({ ...treeParams, branches: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Depth: ", treeParams.depth, jsx("input", { type: "range", min: 1, max: 5, value: treeParams.depth, onChange: (e) => setTreeParams({ ...treeParams, depth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Angle: ", treeParams.branchAngle, "\u00B0", jsx("input", { type: "range", min: 10, max: 80, value: treeParams.branchAngle, onChange: (e) => setTreeParams({ ...treeParams, branchAngle: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", treeParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: treeParams.seed, onChange: (e) => setTreeParams({ ...treeParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'rock' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Radius: ", rockParams.radius.toFixed(1), jsx("input", { type: "range", min: 0.2, max: 5, step: 0.1, value: rockParams.radius, onChange: (e) => setRockParams({ ...rockParams, radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Detail: ", rockParams.detail, jsx("input", { type: "range", min: 1, max: 5, value: rockParams.detail, onChange: (e) => setRockParams({ ...rockParams, detail: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Noise: ", rockParams.noiseAmount.toFixed(2), jsx("input", { type: "range", min: 0, max: 0.5, step: 0.01, value: rockParams.noiseAmount, onChange: (e) => setRockParams({ ...rockParams, noiseAmount: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Seed: ", rockParams.seed, jsx("input", { type: "range", min: 1, max: 999, value: rockParams.seed, onChange: (e) => setRockParams({ ...rockParams, seed: parseInt(e.target.value, 10) }), style: { width: '100%' } })] })] })), type === 'building' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Width: ", buildingParams.width, jsx("input", { type: "range", min: 4, max: 20, step: 1, value: buildingParams.width, onChange: (e) => setBuildingParams({ ...buildingParams, width: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Floors: ", buildingParams.floors, jsx("input", { type: "range", min: 1, max: 20, value: buildingParams.floors, onChange: (e) => setBuildingParams({ ...buildingParams, floors: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Windows/Floor: ", buildingParams.windowsPerFloor, jsx("input", { type: "range", min: 1, max: 10, value: buildingParams.windowsPerFloor, onChange: (e) => setBuildingParams({
|
|
297
299
|
...buildingParams,
|
|
298
300
|
windowsPerFloor: parseInt(e.target.value, 10),
|
|
299
301
|
}), style: { width: '100%' } })] }), jsxs("label", { children: ["Roof:", jsxs("select", { value: buildingParams.roofStyle, onChange: (e) => setBuildingParams({
|
|
300
302
|
...buildingParams,
|
|
301
303
|
roofStyle: e.target.value,
|
|
302
|
-
}), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children:
|
|
304
|
+
}), style: { marginLeft: 8 }, children: [jsx("option", { value: "flat", children: t('proceduralGeometry.flat', 'Flat') }), jsx("option", { value: "gable", children: t('proceduralGeometry.gable', 'Gable') }), jsx("option", { value: "hip", children: "Hip" })] })] })] })), type === 'gear' && (jsxs("div", { style: { display: 'grid', gap: 4 }, children: [jsxs("label", { children: ["Outer Radius: ", gearParams.outerRadius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 5, step: 0.1, value: gearParams.outerRadius, onChange: (e) => setGearParams({ ...gearParams, outerRadius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Teeth: ", gearParams.teeth, jsx("input", { type: "range", min: 4, max: 30, value: gearParams.teeth, onChange: (e) => setGearParams({ ...gearParams, teeth: parseInt(e.target.value, 10) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Tooth Depth: ", gearParams.toothDepth.toFixed(2), jsx("input", { type: "range", min: 0.05, max: 1, step: 0.05, value: gearParams.toothDepth, onChange: (e) => setGearParams({ ...gearParams, toothDepth: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Thickness: ", gearParams.thickness.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 2, step: 0.1, value: gearParams.thickness, onChange: (e) => setGearParams({ ...gearParams, thickness: parseFloat(e.target.value) }), style: { width: '100%' } })] })] })), jsxs("button", { type: "button", onClick: generate, style: {
|
|
303
305
|
background: 'var(--nice-color-accent, #6366f1)',
|
|
304
306
|
color: 'var(--nice-text-on-primary, #fff)',
|
|
305
307
|
border: 'none',
|
|
306
308
|
borderRadius: 4,
|
|
307
309
|
padding: '6px 12px',
|
|
308
310
|
cursor: 'pointer',
|
|
309
|
-
}, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " Generate
|
|
311
|
+
}, children: [jsx(NiceIcon, { name: "wand", size: 14 }), " ", t('proceduralGeometry.generate', 'Generate')] })] }));
|
|
310
312
|
};
|
|
311
313
|
|
|
312
314
|
export { NiceProceduralGeometry, generateBuilding, generateGear, generatePipe, generateRock, generateTree, useProceduralGeometry };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceTutorialButton, resolveTutorial, NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
|
|
7
8
|
const TERRAIN_EDITOR_TOUR = [
|
|
@@ -319,6 +320,7 @@ function useTerrainEditor(scene, camera, canvas, config = { width: 100, depth: 1
|
|
|
319
320
|
}
|
|
320
321
|
/* ── Component ── */
|
|
321
322
|
const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrainChange, className = '', tutorial, }) => {
|
|
323
|
+
const { t } = useNiceTranslation();
|
|
322
324
|
const config = {
|
|
323
325
|
width: 100,
|
|
324
326
|
depth: 100,
|
|
@@ -329,14 +331,14 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
|
|
|
329
331
|
const { brush, setBrush, isPainting, startPainting, stopPainting, generateFlat, generateNoise, exportHeightmap, textureLayers, setTextureLayer, activeTextureLayer, setActiveTextureLayer, } = useTerrainEditor(scene, camera, canvas, config, onTerrainChange);
|
|
330
332
|
const [noiseOctaves, setNoiseOctaves] = useState(6);
|
|
331
333
|
const [noisePersistence, setNoisePersistence] = useState(0.5);
|
|
332
|
-
return (jsxs("div", { className: `nice-terrain-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children:
|
|
334
|
+
return (jsxs("div", { className: `nice-terrain-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: t('terrainEditor.terrainEditor', 'Terrain Editor') }), jsxs("fieldset", { style: {
|
|
333
335
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
334
336
|
borderRadius: 6,
|
|
335
337
|
padding: 4,
|
|
336
338
|
display: 'flex',
|
|
337
339
|
flexWrap: 'wrap',
|
|
338
340
|
gap: 2,
|
|
339
|
-
}, children: [jsx("legend", { style: { fontSize: 11 }, children:
|
|
341
|
+
}, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.brush', 'Brush') }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
|
|
340
342
|
padding: '3px 6px',
|
|
341
343
|
fontSize: 11,
|
|
342
344
|
cursor: 'pointer',
|
|
@@ -353,19 +355,19 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
|
|
|
353
355
|
borderRadius: 4,
|
|
354
356
|
padding: '6px 12px',
|
|
355
357
|
cursor: 'pointer',
|
|
356
|
-
}, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting
|
|
358
|
+
}, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('terrainEditor.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " ", t('terrainEditor.startPainting', 'Start Painting')] })) }), jsxs("fieldset", { style: {
|
|
357
359
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
358
360
|
borderRadius: 6,
|
|
359
361
|
padding: 6,
|
|
360
362
|
display: 'grid',
|
|
361
363
|
gap: 4,
|
|
362
|
-
}, children: [jsx("legend", { style: { fontSize: 11 }, children:
|
|
364
|
+
}, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.procedural', 'Procedural') }), jsxs("label", { children: ["Octaves: ", noiseOctaves, jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: ["Persistence: ", noisePersistence.toFixed(2), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), ' ', t('terrainEditor.generateNoise', 'Generate Noise')] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " ", t('terrainEditor.flatten', 'Flatten')] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
|
|
363
365
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
364
366
|
borderRadius: 6,
|
|
365
367
|
padding: 6,
|
|
366
368
|
display: 'grid',
|
|
367
369
|
gap: 4,
|
|
368
|
-
}, children: [jsx("legend", { style: { fontSize: 11 }, children:
|
|
370
|
+
}, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.textureLayers', 'Texture Layers') }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
|
|
369
371
|
flex: 1,
|
|
370
372
|
padding: '2px 6px',
|
|
371
373
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
@@ -378,7 +380,7 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
|
|
|
378
380
|
link.href = c.toDataURL('image/png');
|
|
379
381
|
link.click();
|
|
380
382
|
}
|
|
381
|
-
}, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }),
|
|
383
|
+
}, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), ' ', t('terrainEditor.exportHeightmapPng', 'Export Heightmap PNG')] })] }));
|
|
382
384
|
};
|
|
383
385
|
|
|
384
386
|
export { NiceTerrainEditor, useTerrainEditor };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/* ── Weight painting utilities ── */
|
|
7
8
|
function getFalloff(distance, radius, type) {
|
|
@@ -225,6 +226,7 @@ function updateHeatMapColors(mesh, boneIndex, colors) {
|
|
|
225
226
|
}
|
|
226
227
|
/* ── Component ── */
|
|
227
228
|
const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, scene, camera, canvas, onWeightsChange, className = '', }) => {
|
|
229
|
+
const { t } = useNiceTranslation();
|
|
228
230
|
const { brush, setBrush, isPainting, activeBoneIndex, setActiveBoneIndex, boneNames, startPainting, stopPainting, normalizeWeights, showHeatMap, setShowHeatMap, } = useWeightPainter(skinnedMesh, scene, camera, canvas, onWeightsChange);
|
|
229
231
|
useEffect(() => {
|
|
230
232
|
setActiveBoneIndex(initialBoneIndex);
|
|
@@ -235,7 +237,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
|
|
|
235
237
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
236
238
|
borderRadius: 6,
|
|
237
239
|
padding: 4,
|
|
238
|
-
}, children: [jsx("legend", { style: { fontSize: 11 }, children:
|
|
240
|
+
}, children: [jsx("legend", { style: { fontSize: 11 }, children: t('weightPainter.mode', 'Mode') }), ['add', 'subtract', 'smooth', 'blur'].map((mode) => (jsx("button", { type: "button", onClick: () => setBrush({ mode }), style: {
|
|
239
241
|
flex: 1,
|
|
240
242
|
padding: '4px 6px',
|
|
241
243
|
background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
@@ -254,7 +256,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
|
|
|
254
256
|
borderRadius: 4,
|
|
255
257
|
padding: '6px 12px',
|
|
256
258
|
cursor: 'pointer',
|
|
257
|
-
}, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Stop Painting
|
|
259
|
+
}, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('weightPainter.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), ' ', t('weightPainter.startPainting', 'Start Painting')] })) }), jsxs("button", { type: "button", onClick: normalizeWeights, title: t('weightPainter.normalizeAllWeights', 'Normalize all weights'), children: [jsx(NiceIcon, { name: "scale", size: 14 }), " ", t('weightPainter.normalize', 'Normalize')] })] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: showHeatMap, onChange: (e) => setShowHeatMap(e.target.checked) }), t('weightPainter.showWeightHeatMap', 'Show weight heat map')] })] }));
|
|
258
260
|
};
|
|
259
261
|
|
|
260
262
|
export { NiceWeightPainter, useWeightPainter };
|
|
@@ -2,6 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { NiceIcon } from '@nice2dev/ui';
|
|
3
3
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
|
+
import { useNiceTranslation } from '../core/i18n.js';
|
|
5
6
|
|
|
6
7
|
/* ── Hook ── */
|
|
7
8
|
function useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd) {
|
|
@@ -259,13 +260,14 @@ function useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd) {
|
|
|
259
260
|
}
|
|
260
261
|
/* ── Component ── */
|
|
261
262
|
const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionStart, onSessionEnd, className = '', }) => {
|
|
263
|
+
const { t } = useNiceTranslation();
|
|
262
264
|
const { isSupported, isSessionActive, mode, setMode, startSession, endSession, controllers, teleportEnabled, setTeleportEnabled, hitTestResult, } = useXRPreview(renderer, scene, camera, onSessionStart, onSessionEnd);
|
|
263
265
|
useEffect(() => {
|
|
264
266
|
if (initialMode) {
|
|
265
267
|
setMode(initialMode);
|
|
266
268
|
}
|
|
267
269
|
}, [initialMode, setMode]);
|
|
268
|
-
return (jsxs("div", { className: `nice-xr-preview ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "eye", size: 14 }), " XR Preview
|
|
270
|
+
return (jsxs("div", { className: `nice-xr-preview ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "eye", size: 14 }), " ", t('xrPreview.xrPreview', 'XR Preview')] }), !isSupported && (jsx("div", { style: { color: 'var(--nice-danger, #ef4444)', fontSize: 11 }, children: t('xrPreview.webxrIsNotSupportedIn', 'WebXR is not supported in this browser.') })), isSupported && (jsxs(Fragment, { children: [jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => setMode('vr'), style: {
|
|
269
271
|
flex: 1,
|
|
270
272
|
padding: '5px 8px',
|
|
271
273
|
borderRadius: 4,
|
|
@@ -290,7 +292,7 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
|
|
|
290
292
|
borderRadius: 4,
|
|
291
293
|
padding: '6px 12px',
|
|
292
294
|
cursor: 'pointer',
|
|
293
|
-
}, children: isSessionActive ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " Exit XR
|
|
295
|
+
}, children: isSessionActive ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('xrPreview.exitXr', 'Exit XR')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", t('xrPreview.enterXr', 'Enter XR')] })) }), mode === 'vr' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "checkbox", checked: teleportEnabled, onChange: (e) => setTeleportEnabled(e.target.checked) }), t('xrPreview.teleportation', 'Teleportation')] })), isSessionActive && (jsxs("div", { style: { fontSize: 11, opacity: 0.7 }, children: ["Controllers: ", controllers.length, jsx("br", {}), mode === 'ar' && hitTestResult && (jsxs(Fragment, { children: ["Hit: (", hitTestResult.x.toFixed(2), ", ", hitTestResult.y.toFixed(2), ",", ' ', hitTestResult.z.toFixed(2), ")"] }))] }))] }))] }));
|
|
294
296
|
};
|
|
295
297
|
|
|
296
298
|
export { NiceXRPreview, useXRPreview };
|
|
@@ -21,6 +21,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
|
21
21
|
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
|
+
import { useEditorAsk } from '../core/editorAsk.js';
|
|
24
25
|
import { createFrameClock, SOFT_SHADOW_MAP } from '../core/threeCompat.js';
|
|
25
26
|
import { SUPPORTED_EXTENSIONS, pose3dToClip } from './modelEditorTypes.js';
|
|
26
27
|
import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstanced, applyHDRIPreset, snapMulti, HDRI_PRESETS, PBR_PRESETS } from './modelEditorUtils.js';
|
|
@@ -32,7 +33,7 @@ import { UndoRedoStack, createMaterialFromPreset, generateLOD, convertToInstance
|
|
|
32
33
|
*
|
|
33
34
|
* Extracted from the original ModelEditor.tsx god-component.
|
|
34
35
|
*/
|
|
35
|
-
const log = { error: console.error, warn: console.warn
|
|
36
|
+
const log = { error: console.error, warn: console.warn };
|
|
36
37
|
/* helpers */
|
|
37
38
|
let _nid = 0;
|
|
38
39
|
const nid = () => `n${++_nid}`;
|
|
@@ -54,6 +55,7 @@ function findNodeById(nodes, id) {
|
|
|
54
55
|
═══════════════════════════════════════════ */
|
|
55
56
|
function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
56
57
|
var _a;
|
|
58
|
+
const { pendingAsk, ask } = useEditorAsk();
|
|
57
59
|
/* ── refs ── */
|
|
58
60
|
const mountRef = useRef(null);
|
|
59
61
|
const rendererRef = useRef(null);
|
|
@@ -204,10 +206,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
204
206
|
// goes into the scene is its helper.
|
|
205
207
|
const tcHelper = tc.getHelper();
|
|
206
208
|
tcHelper.name = '__transformControls';
|
|
207
|
-
|
|
208
|
-
tc.addEventListener('dragging-changed', ((e) => {
|
|
209
|
+
tc.addEventListener('dragging-changed', (e) => {
|
|
209
210
|
orbit.enabled = !e.value;
|
|
210
|
-
})
|
|
211
|
+
});
|
|
211
212
|
scene.add(tcHelper);
|
|
212
213
|
transformRef.current = tc;
|
|
213
214
|
// Resize handler
|
|
@@ -1226,11 +1227,8 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1226
1227
|
}
|
|
1227
1228
|
});
|
|
1228
1229
|
}, []);
|
|
1229
|
-
const
|
|
1230
|
+
const clearSceneNow = useCallback(() => {
|
|
1230
1231
|
var _a;
|
|
1231
|
-
if (!window.confirm('Clear the entire scene? This cannot be undone.')) {
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
1232
|
const scene = sceneRef.current;
|
|
1235
1233
|
const toRemove = [];
|
|
1236
1234
|
scene.traverse((obj) => {
|
|
@@ -1262,6 +1260,14 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1262
1260
|
rebuildTree();
|
|
1263
1261
|
setStatusText('Scene cleared');
|
|
1264
1262
|
}, [rebuildTree, disposeObject]);
|
|
1263
|
+
// asks first: clearing cannot be undone
|
|
1264
|
+
const clearScene = useCallback(() => {
|
|
1265
|
+
void ask('confirm', 'Clear the entire scene? This cannot be undone.').then((answer) => {
|
|
1266
|
+
if (answer !== null) {
|
|
1267
|
+
clearSceneNow();
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}, [ask, clearSceneNow]);
|
|
1265
1271
|
/* ─────────────────────────────────────────
|
|
1266
1272
|
Save to Library
|
|
1267
1273
|
───────────────────────────────────────── */
|
|
@@ -1279,19 +1285,19 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1279
1285
|
const blob = new Blob([result], { type: 'model/gltf-binary' });
|
|
1280
1286
|
const reader = new FileReader();
|
|
1281
1287
|
reader.onload = () => {
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
+
const dataUrl = reader.result;
|
|
1289
|
+
void ask('prompt', 'Save to library as:', 'model.glb').then((name) => {
|
|
1290
|
+
if (name && name.trim()) {
|
|
1291
|
+
onSaveToLibrary(dataUrl, name.trim(), 'model/gltf-binary');
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1288
1294
|
};
|
|
1289
1295
|
reader.readAsDataURL(blob);
|
|
1290
1296
|
}, (err) => {
|
|
1291
1297
|
log.error('Save to library error:', err);
|
|
1292
1298
|
setStatusText(`Save error: ${String(err)}`);
|
|
1293
1299
|
}, { binary: true, animations: allClipsRef.current.map((a) => a.clip) });
|
|
1294
|
-
}, [onSaveToLibrary]);
|
|
1300
|
+
}, [onSaveToLibrary, ask]);
|
|
1295
1301
|
/* ─────────────────────────────────────────
|
|
1296
1302
|
Add Mesh / Light / Camera / Empty
|
|
1297
1303
|
───────────────────────────────────────── */
|
|
@@ -1787,6 +1793,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
1787
1793
|
bgColor,
|
|
1788
1794
|
setBgColor,
|
|
1789
1795
|
statusText,
|
|
1796
|
+
pendingAsk,
|
|
1790
1797
|
dragOver,
|
|
1791
1798
|
polyCount,
|
|
1792
1799
|
meshCount,
|