@nice2dev/ui-3d 1.1.10 → 1.2.0
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 +70 -0
- package/README.md +11 -15
- package/dist/cjs/core/threeCompat.js +52 -0
- package/dist/cjs/dance/PoseDetector.js +112 -62
- package/dist/cjs/index.js +0 -3
- package/dist/cjs/model/ModelEditor.js +0 -3
- package/dist/cjs/model/ModelViewer.js +0 -3
- package/dist/cjs/model/NiceArmatureEditor.js +2 -2
- package/dist/cjs/model/NicePhysicsSimulation.js +1 -1
- package/dist/cjs/model/NiceTerrainEditor.js +2 -2
- package/dist/cjs/model/NiceWeightPainter.js +2 -2
- package/dist/cjs/model/NiceXRPreview.js +3 -3
- package/dist/cjs/model/useModelEditor.js +8 -3
- package/dist/cjs/model/useModelViewer.js +13 -9
- package/dist/cjs/rendering/NiceRenderExport.js +2 -2
- package/dist/cjs/rendering/NiceWebGPURenderer.js +3 -2
- package/dist/esm/core/threeCompat.js +30 -0
- package/dist/esm/dance/PoseDetector.js +114 -63
- package/dist/esm/index.js +3 -3
- package/dist/esm/model/ModelEditor.js +1 -3
- package/dist/esm/model/ModelViewer.js +1 -3
- package/dist/esm/model/NiceArmatureEditor.js +3 -3
- package/dist/esm/model/NicePhysicsSimulation.js +1 -1
- package/dist/esm/model/NiceTerrainEditor.js +2 -2
- package/dist/esm/model/NiceWeightPainter.js +2 -2
- package/dist/esm/model/NiceXRPreview.js +3 -3
- package/dist/esm/model/useModelEditor.js +8 -3
- package/dist/esm/model/useModelViewer.js +13 -9
- package/dist/esm/rendering/NiceRenderExport.js +2 -2
- package/dist/esm/rendering/NiceWebGPURenderer.js +3 -2
- package/dist/types/core/threeCompat.d.ts +14 -0
- package/dist/types/dance/PoseDetector.d.ts +14 -13
- package/dist/types/index.d.ts +4 -4
- package/dist/types/model/ModelEditor.d.ts +0 -4
- package/dist/types/model/ModelViewer.d.ts +0 -4
- package/package.json +2 -2
|
@@ -23,6 +23,7 @@ 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 threeCompat = require('../core/threeCompat.js');
|
|
26
27
|
var modelEditorTypes = require('./modelEditorTypes.js');
|
|
27
28
|
var modelEditorUtils = require('./modelEditorUtils.js');
|
|
28
29
|
|
|
@@ -73,6 +74,7 @@ function findNodeById(nodes, id) {
|
|
|
73
74
|
Hook
|
|
74
75
|
═══════════════════════════════════════════ */
|
|
75
76
|
function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
77
|
+
var _a;
|
|
76
78
|
/* ── refs ── */
|
|
77
79
|
const mountRef = React.useRef(null);
|
|
78
80
|
const rendererRef = React.useRef(null);
|
|
@@ -80,7 +82,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
80
82
|
const cameraRef = React.useRef(new THREE__namespace.PerspectiveCamera(50, 1, 0.01, 10000));
|
|
81
83
|
const orbitRef = React.useRef(null);
|
|
82
84
|
const transformRef = React.useRef(null);
|
|
83
|
-
|
|
85
|
+
// Created once: useRef's argument runs on every render, and three r183 warns per Clock.
|
|
86
|
+
const clockRef = React.useRef(null);
|
|
87
|
+
(_a = clockRef.current) !== null && _a !== void 0 ? _a : (clockRef.current = threeCompat.createFrameClock());
|
|
84
88
|
const mixerRef = React.useRef(null);
|
|
85
89
|
const rafRef = React.useRef(0);
|
|
86
90
|
const fileInputRef = React.useRef(null);
|
|
@@ -160,7 +164,7 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
160
164
|
const renderer = new THREE__namespace.WebGLRenderer({ antialias: true, alpha: false });
|
|
161
165
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
162
166
|
renderer.shadowMap.enabled = true;
|
|
163
|
-
renderer.shadowMap.type =
|
|
167
|
+
renderer.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
|
|
164
168
|
renderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
|
|
165
169
|
renderer.toneMappingExposure = 1.2;
|
|
166
170
|
renderer.outputColorSpace = THREE__namespace.SRGBColorSpace;
|
|
@@ -243,8 +247,9 @@ function useModelEditor({ onSaveToLibrary, onAIPoseVideo, }) {
|
|
|
243
247
|
onResize();
|
|
244
248
|
// Animation loop
|
|
245
249
|
const animate = () => {
|
|
250
|
+
var _a, _b;
|
|
246
251
|
rafRef.current = requestAnimationFrame(animate);
|
|
247
|
-
const dt = clockRef.current.getDelta();
|
|
252
|
+
const dt = (_b = (_a = clockRef.current) === null || _a === void 0 ? void 0 : _a.getDelta()) !== null && _b !== void 0 ? _b : 0;
|
|
248
253
|
orbit.update();
|
|
249
254
|
// Update mixer
|
|
250
255
|
if (mixerRef.current && isPlayingRef.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 threeCompat = require('../core/threeCompat.js');
|
|
13
14
|
|
|
14
15
|
function _interopNamespaceDefault(e) {
|
|
15
16
|
var n = Object.create(null);
|
|
@@ -60,6 +61,7 @@ const TONE_MAP = {
|
|
|
60
61
|
Hook
|
|
61
62
|
═══════════════════════════════════════════ */
|
|
62
63
|
function useModelViewer(props = {}) {
|
|
64
|
+
var _a;
|
|
63
65
|
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;
|
|
64
66
|
/* ── refs ── */
|
|
65
67
|
const mountRef = React.useRef(null);
|
|
@@ -67,7 +69,9 @@ function useModelViewer(props = {}) {
|
|
|
67
69
|
const sceneRef = React.useRef(new THREE__namespace.Scene());
|
|
68
70
|
const cameraRef = React.useRef(new THREE__namespace.PerspectiveCamera(fov, 1, 0.01, 10000));
|
|
69
71
|
const orbitRef = React.useRef(null);
|
|
70
|
-
|
|
72
|
+
// Created once: useRef's argument runs on every render, and three r183 warns per Clock.
|
|
73
|
+
const clockRef = React.useRef(null);
|
|
74
|
+
(_a = clockRef.current) !== null && _a !== void 0 ? _a : (clockRef.current = threeCompat.createFrameClock());
|
|
71
75
|
const mixerRef = React.useRef(null);
|
|
72
76
|
const rafRef = React.useRef(0);
|
|
73
77
|
const rootRef = React.useRef(null);
|
|
@@ -123,7 +127,7 @@ function useModelViewer(props = {}) {
|
|
|
123
127
|
renderer.outputColorSpace = THREE__namespace.SRGBColorSpace;
|
|
124
128
|
if (shadows) {
|
|
125
129
|
renderer.shadowMap.enabled = true;
|
|
126
|
-
renderer.shadowMap.type =
|
|
130
|
+
renderer.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
|
|
127
131
|
}
|
|
128
132
|
mount.appendChild(renderer.domElement);
|
|
129
133
|
rendererRef.current = renderer;
|
|
@@ -177,20 +181,20 @@ function useModelViewer(props = {}) {
|
|
|
177
181
|
ro.observe(mount);
|
|
178
182
|
// Animation loop
|
|
179
183
|
const animate = () => {
|
|
180
|
-
var _a, _b, _c, _d, _e;
|
|
184
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
181
185
|
if (disposeRef.current) {
|
|
182
186
|
return;
|
|
183
187
|
}
|
|
184
188
|
rafRef.current = requestAnimationFrame(animate);
|
|
185
|
-
const dt = clockRef.current.getDelta();
|
|
189
|
+
const dt = (_b = (_a = clockRef.current) === null || _a === void 0 ? void 0 : _a.getDelta()) !== null && _b !== void 0 ? _b : 0;
|
|
186
190
|
// Gamepad FPS-style nav
|
|
187
191
|
if (gamepad && navMode === 'fps') {
|
|
188
|
-
const pads = (
|
|
189
|
-
const gp = (
|
|
192
|
+
const pads = (_c = navigator.getGamepads) === null || _c === void 0 ? void 0 : _c.call(navigator);
|
|
193
|
+
const gp = (_d = pads === null || pads === void 0 ? void 0 : pads[0]) !== null && _d !== void 0 ? _d : null;
|
|
190
194
|
if (gp) {
|
|
191
|
-
const lx = (
|
|
192
|
-
const ly = (
|
|
193
|
-
const rx = (
|
|
195
|
+
const lx = (_e = gp.axes[0]) !== null && _e !== void 0 ? _e : 0;
|
|
196
|
+
const ly = (_f = gp.axes[1]) !== null && _f !== void 0 ? _f : 0;
|
|
197
|
+
const rx = (_g = gp.axes[2]) !== null && _g !== void 0 ? _g : 0;
|
|
194
198
|
const fwd = new THREE__namespace.Vector3();
|
|
195
199
|
camera.getWorldDirection(fwd);
|
|
196
200
|
fwd.y = 0;
|
|
@@ -341,13 +341,13 @@ const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
|
|
|
341
341
|
? 'var(--nice-color-accent, #6366f1)'
|
|
342
342
|
: 'transparent',
|
|
343
343
|
color: config.width === p.width && config.height === p.height
|
|
344
|
-
? 'var(--nice-
|
|
344
|
+
? 'var(--nice-color-white, #fff)'
|
|
345
345
|
: 'inherit',
|
|
346
346
|
}, children: p.label }, p.label))) }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsxRuntime.jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsxRuntime.jsx("span", { children: "\u00D7" }), jsxRuntime.jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxRuntime.jsxs("label", { children: ["Format:", jsxRuntime.jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "png", children: "PNG (lossless + alpha)" }), jsxRuntime.jsx("option", { value: "jpeg", children: "JPEG (lossy, no alpha)" }), jsxRuntime.jsx("option", { value: "exr", children: "EXR (HDR float)" })] })] }), jsxRuntime.jsxs("label", { children: ["Background:", jsxRuntime.jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: "transparent", children: "Transparent" }), jsxRuntime.jsx("option", { value: "color", children: "Solid Color" }), jsxRuntime.jsx("option", { value: "scene", children: "Scene Background" })] })] }), config.backgroundMode === 'color' && (jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: ["Color:", jsxRuntime.jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxRuntime.jsxs("label", { children: ["Supersampling: ", config.samples, "x", jsxRuntime.jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: 1, children: "1x (none)" }), jsxRuntime.jsx("option", { value: 2, children: "2x" }), jsxRuntime.jsx("option", { value: 4, children: "4x (best quality)" })] })] }), jsxRuntime.jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
|
|
347
347
|
background: isRendering
|
|
348
348
|
? 'var(--nice-bg-tertiary, #94a3b8)'
|
|
349
349
|
: 'var(--nice-color-accent, #6366f1)',
|
|
350
|
-
color: 'var(--nice-
|
|
350
|
+
color: 'var(--nice-color-white, #fff)',
|
|
351
351
|
border: 'none',
|
|
352
352
|
borderRadius: 4,
|
|
353
353
|
padding: '8px 12px',
|
|
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
4
4
|
var ui = require('@nice2dev/ui');
|
|
5
5
|
var React = require('react');
|
|
6
6
|
var THREE = require('three');
|
|
7
|
+
var threeCompat = require('../core/threeCompat.js');
|
|
7
8
|
|
|
8
9
|
function _interopNamespaceDefault(e) {
|
|
9
10
|
var n = Object.create(null);
|
|
@@ -137,7 +138,7 @@ function useWebGPURenderer(canvas, scene, camera, width = 800, height = 600, onR
|
|
|
137
138
|
r.toneMappingExposure = toneMappingExposure;
|
|
138
139
|
r.outputColorSpace = outputColorSpace;
|
|
139
140
|
r.shadowMap.enabled = true;
|
|
140
|
-
r.shadowMap.type =
|
|
141
|
+
r.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
|
|
141
142
|
rendererRef.current = r;
|
|
142
143
|
setRenderer(r);
|
|
143
144
|
setIsWebGPU(usingWebGPU);
|
|
@@ -235,7 +236,7 @@ const NiceWebGPURenderer = ({ canvas, scene, camera, width, height, onRendererRe
|
|
|
235
236
|
padding: '2px 6px',
|
|
236
237
|
borderRadius: 4,
|
|
237
238
|
background: isWebGPU ? 'var(--nice-success, #22c55e)' : 'var(--nice-warning, #f59e0b)',
|
|
238
|
-
color: 'var(--nice-
|
|
239
|
+
color: 'var(--nice-color-white, #fff)',
|
|
239
240
|
}, children: isWebGPU ? 'WebGPU' : 'WebGL' })] }), !isReady && jsxRuntime.jsx("div", { style: { opacity: 0.6, fontSize: 11 }, children: "Initializing\u2026" }), capabilities && (jsxRuntime.jsxs("div", { style: { fontSize: 10, opacity: 0.7, lineHeight: 1.5 }, children: ["GPU: ", capabilities.adapterName, jsxRuntime.jsx("br", {}), "Vendor: ", capabilities.vendor, jsxRuntime.jsx("br", {}), "Max Texture: ", capabilities.maxTextureDimension2D, "px", capabilities.features.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("br", {}), "Features: ", capabilities.features.length] }))] })), jsxRuntime.jsxs("label", { children: ["Tone Mapping:", jsxRuntime.jsx("select", { value: toneMapping, onChange: (e) => setToneMapping(parseInt(e.target.value, 10)), style: { width: '100%', marginTop: 2 }, children: TONE_MAPPINGS.map((tm) => (jsxRuntime.jsx("option", { value: tm.value, children: tm.label }, tm.value))) })] }), jsxRuntime.jsxs("label", { children: ["Exposure: ", toneMappingExposure.toFixed(2), jsxRuntime.jsx("input", { type: "range", min: 0.1, max: 3, step: 0.05, value: toneMappingExposure, onChange: (e) => setToneMappingExposure(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxRuntime.jsxs("label", { children: ["Output Color Space:", jsxRuntime.jsxs("select", { value: outputColorSpace, onChange: (e) => setOutputColorSpace(e.target.value), style: { width: '100%', marginTop: 2 }, children: [jsxRuntime.jsx("option", { value: THREE__namespace.SRGBColorSpace, children: "sRGB" }), jsxRuntime.jsx("option", { value: THREE__namespace.LinearSRGBColorSpace, children: "Linear sRGB" })] })] }), jsxRuntime.jsxs("label", { children: ["Pixel Ratio: ", pixelRatio.toFixed(1), jsxRuntime.jsx("input", { type: "range", min: 0.5, max: 3, step: 0.5, value: pixelRatio, onChange: (e) => setPixelRatio(parseFloat(e.target.value)), style: { width: '100%' } })] })] }));
|
|
240
241
|
};
|
|
241
242
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* three.js r183 deprecated `Clock` (for `Timer`) and folds `PCFSoftShadowMap` into
|
|
5
|
+
* `PCFShadowMap`; both log a console warning there. The peer range starts at r150, which has
|
|
6
|
+
* neither `Timer` in its core nor the softer `PCFShadowMap`, so the choice follows the revision.
|
|
7
|
+
*/
|
|
8
|
+
const REVISION = Number.parseInt(THREE.REVISION, 10);
|
|
9
|
+
const MODERN = REVISION >= 183;
|
|
10
|
+
/** Soft shadows without the r183 deprecation warning; older three keeps `PCFSoftShadowMap`. */
|
|
11
|
+
const SOFT_SHADOW_MAP = MODERN
|
|
12
|
+
? THREE.PCFShadowMap
|
|
13
|
+
: THREE.PCFSoftShadowMap;
|
|
14
|
+
/** `Timer` where three has deprecated `Clock`, `Clock` before. */
|
|
15
|
+
function createFrameClock() {
|
|
16
|
+
const TimerCtor = THREE.Timer;
|
|
17
|
+
if (MODERN && TimerCtor) {
|
|
18
|
+
const timer = new TimerCtor();
|
|
19
|
+
return {
|
|
20
|
+
getDelta: () => {
|
|
21
|
+
timer.update();
|
|
22
|
+
return timer.getDelta();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const clock = new THREE.Clock();
|
|
27
|
+
return { getDelta: () => clock.getDelta() };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { SOFT_SHADOW_MAP, createFrameClock };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { resolveCssColor } from '@nice2dev/ui';
|
|
3
|
-
import { forwardRef, useRef, useImperativeHandle, useCallback,
|
|
3
|
+
import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
// ── Standard keypoint definitions (COCO 17-keypoint model) ──────
|
|
6
6
|
const COCO_KEYPOINTS = [
|
|
@@ -40,19 +40,15 @@ const SKELETON_CONNECTIONS = [
|
|
|
40
40
|
[12, 14],
|
|
41
41
|
[14, 16], // right leg
|
|
42
42
|
];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return COCO_KEYPOINTS.map((name, _i) => ({
|
|
46
|
-
name,
|
|
47
|
-
x: 0,
|
|
48
|
-
y: 0,
|
|
49
|
-
z: 0,
|
|
50
|
-
confidence: 0,
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
43
|
+
/** A keypoint the detector reported without a score counts as found. */
|
|
44
|
+
const confidenceOf = (kp) => { var _a; return (_a = kp.confidence) !== null && _a !== void 0 ? _a : 1; };
|
|
53
45
|
// ── Hook: usePoseDetection ──────────────────────────────────────
|
|
54
46
|
function usePoseDetection(config = {}) {
|
|
55
|
-
|
|
47
|
+
// The detection loop outlives renders: it reads the latest settings from here.
|
|
48
|
+
const configRef = useRef(config);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
configRef.current = config;
|
|
51
|
+
});
|
|
56
52
|
const [isDetecting, setIsDetecting] = useState(false);
|
|
57
53
|
const latestFrameRef = useRef(null);
|
|
58
54
|
const frameIndexRef = useRef(0);
|
|
@@ -60,33 +56,67 @@ function usePoseDetection(config = {}) {
|
|
|
60
56
|
const streamRef = useRef(null);
|
|
61
57
|
const videoRef = useRef(null);
|
|
62
58
|
const callbackRef = useRef(null);
|
|
63
|
-
const
|
|
59
|
+
const errorRef = useRef(null);
|
|
60
|
+
const startedAtRef = useRef(0);
|
|
61
|
+
const lastRunRef = useRef(-Infinity);
|
|
62
|
+
const busyRef = useRef(false);
|
|
63
|
+
// Bumped by start() and stop(): a detection that resolves after them is dropped.
|
|
64
|
+
const runRef = useRef(0);
|
|
65
|
+
const stop = useCallback(() => {
|
|
64
66
|
var _a;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
runRef.current += 1;
|
|
68
|
+
cancelAnimationFrame(rafRef.current);
|
|
69
|
+
(_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
|
|
70
|
+
streamRef.current = null;
|
|
71
|
+
if (videoRef.current) {
|
|
72
|
+
videoRef.current.srcObject = null;
|
|
73
|
+
}
|
|
74
|
+
setIsDetecting(false);
|
|
75
|
+
}, []);
|
|
76
|
+
const processFrame = useCallback(() => {
|
|
77
|
+
rafRef.current = requestAnimationFrame(processFrame);
|
|
78
|
+
const video = videoRef.current;
|
|
79
|
+
const { detector, maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = configRef.current;
|
|
80
|
+
if (!detector || !video || video.readyState < 2 || busyRef.current) {
|
|
67
81
|
return;
|
|
68
82
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// In production, this calls MediaPipe/TensorFlow.js pose estimation
|
|
73
|
-
const persons = [];
|
|
74
|
-
for (let p = 0; p < maxPersons; p++) {
|
|
75
|
-
const keypoints = createSimulatedKeypoints();
|
|
76
|
-
if (keypoints.some((kp) => { var _a; return ((_a = kp.confidence) !== null && _a !== void 0 ? _a : 0) >= minConfidence; })) {
|
|
77
|
-
persons.push({ id: p, keypoints });
|
|
78
|
-
}
|
|
83
|
+
const now = performance.now();
|
|
84
|
+
if (now - lastRunRef.current < 1000 / targetFps) {
|
|
85
|
+
return;
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
lastRunRef.current = now;
|
|
88
|
+
busyRef.current = true;
|
|
89
|
+
const run = runRef.current;
|
|
90
|
+
Promise.resolve()
|
|
91
|
+
.then(() => detector(video, now))
|
|
92
|
+
.then((found) => {
|
|
93
|
+
var _a;
|
|
94
|
+
if (run !== runRef.current) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const frame = {
|
|
98
|
+
frame_index: frameIndexRef.current++,
|
|
99
|
+
timestamp_sec: (now - startedAtRef.current) / 1000,
|
|
100
|
+
persons: found
|
|
101
|
+
.filter((person) => person.keypoints.some((kp) => confidenceOf(kp) >= minConfidence))
|
|
102
|
+
.slice(0, maxPersons),
|
|
103
|
+
};
|
|
104
|
+
latestFrameRef.current = frame;
|
|
105
|
+
(_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
|
|
106
|
+
})
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
var _a;
|
|
109
|
+
if (run !== runRef.current) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
stop();
|
|
113
|
+
(_a = errorRef.current) === null || _a === void 0 ? void 0 : _a.call(errorRef, err instanceof Error ? err : new Error(String(err)));
|
|
114
|
+
})
|
|
115
|
+
.finally(() => {
|
|
116
|
+
busyRef.current = false;
|
|
117
|
+
});
|
|
118
|
+
}, [stop]);
|
|
119
|
+
const start = useCallback(async (video, onFrame, onError) => {
|
|
90
120
|
try {
|
|
91
121
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
92
122
|
video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } },
|
|
@@ -96,7 +126,14 @@ function usePoseDetection(config = {}) {
|
|
|
96
126
|
video.srcObject = stream;
|
|
97
127
|
await video.play();
|
|
98
128
|
callbackRef.current = onFrame !== null && onFrame !== void 0 ? onFrame : null;
|
|
129
|
+
errorRef.current = onError !== null && onError !== void 0 ? onError : null;
|
|
99
130
|
frameIndexRef.current = 0;
|
|
131
|
+
startedAtRef.current = performance.now();
|
|
132
|
+
lastRunRef.current = -Infinity;
|
|
133
|
+
runRef.current += 1;
|
|
134
|
+
if (!configRef.current.detector) {
|
|
135
|
+
console.warn('[NicePoseDetector] started without a `detector`: the camera runs, but no poses are detected.');
|
|
136
|
+
}
|
|
100
137
|
setIsDetecting(true);
|
|
101
138
|
rafRef.current = requestAnimationFrame(processFrame);
|
|
102
139
|
}
|
|
@@ -104,16 +141,6 @@ function usePoseDetection(config = {}) {
|
|
|
104
141
|
throw new Error(`Camera access failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
142
|
}
|
|
106
143
|
}, [processFrame]);
|
|
107
|
-
const stop = useCallback(() => {
|
|
108
|
-
var _a;
|
|
109
|
-
cancelAnimationFrame(rafRef.current);
|
|
110
|
-
(_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
|
|
111
|
-
streamRef.current = null;
|
|
112
|
-
if (videoRef.current) {
|
|
113
|
-
videoRef.current.srcObject = null;
|
|
114
|
-
}
|
|
115
|
-
setIsDetecting(false);
|
|
116
|
-
}, []);
|
|
117
144
|
useEffect(() => () => {
|
|
118
145
|
stop();
|
|
119
146
|
}, [stop]);
|
|
@@ -134,64 +161,90 @@ const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
|
|
|
134
161
|
},
|
|
135
162
|
}));
|
|
136
163
|
const drawSkeleton = useCallback((frame) => {
|
|
137
|
-
var _a
|
|
164
|
+
var _a;
|
|
138
165
|
const ctx = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getContext('2d');
|
|
139
166
|
if (!ctx || !canvasRef.current) {
|
|
140
167
|
return;
|
|
141
168
|
}
|
|
142
169
|
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
|
170
|
+
// Keypoints are in video pixels; the video fills the box with object-fit: cover.
|
|
171
|
+
const video = videoRef.current;
|
|
172
|
+
const videoW = (video === null || video === void 0 ? void 0 : video.videoWidth) || width;
|
|
173
|
+
const videoH = (video === null || video === void 0 ? void 0 : video.videoHeight) || height;
|
|
174
|
+
const scale = Math.max(width / videoW, height / videoH);
|
|
175
|
+
const offsetX = (width - videoW * scale) / 2;
|
|
176
|
+
const offsetY = (height - videoH * scale) / 2;
|
|
177
|
+
const at = (kp) => [
|
|
178
|
+
kp.x * scale + offsetX,
|
|
179
|
+
kp.y * scale + offsetY,
|
|
180
|
+
];
|
|
143
181
|
for (const person of frame.persons) {
|
|
144
182
|
const kps = person.keypoints;
|
|
183
|
+
const byName = new Map(kps.map((kp) => [kp.name, kp]));
|
|
145
184
|
// Draw connections
|
|
146
185
|
if (showSkeleton) {
|
|
147
186
|
ctx.strokeStyle = resolveCssColor('var(--nice-success, #00ff88)', ctx.canvas);
|
|
148
187
|
ctx.lineWidth = 2;
|
|
149
188
|
for (const [a, b] of SKELETON_CONNECTIONS) {
|
|
150
|
-
const ka =
|
|
189
|
+
const ka = byName.get(COCO_KEYPOINTS[a]);
|
|
190
|
+
const kb = byName.get(COCO_KEYPOINTS[b]);
|
|
151
191
|
if (!ka || !kb) {
|
|
152
192
|
continue;
|
|
153
193
|
}
|
|
154
|
-
if ((
|
|
194
|
+
if (confidenceOf(ka) < 0.3 || confidenceOf(kb) < 0.3) {
|
|
155
195
|
continue;
|
|
156
196
|
}
|
|
157
197
|
ctx.beginPath();
|
|
158
|
-
ctx.moveTo(ka
|
|
159
|
-
ctx.lineTo(kb
|
|
198
|
+
ctx.moveTo(...at(ka));
|
|
199
|
+
ctx.lineTo(...at(kb));
|
|
160
200
|
ctx.stroke();
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
203
|
// Draw keypoints
|
|
164
204
|
for (const kp of kps) {
|
|
165
|
-
if ((
|
|
205
|
+
if (confidenceOf(kp) < 0.3) {
|
|
166
206
|
continue;
|
|
167
207
|
}
|
|
168
|
-
const radius = showConfidence ? 3 + (
|
|
208
|
+
const radius = showConfidence ? 3 + confidenceOf(kp) * 5 : 4;
|
|
169
209
|
ctx.fillStyle = resolveCssColor(showConfidence
|
|
170
|
-
? `rgba(0, 255, 136, ${(
|
|
210
|
+
? `rgba(0, 255, 136, ${confidenceOf(kp)})`
|
|
171
211
|
: 'var(--nice-success, #00ff88)', ctx.canvas);
|
|
172
212
|
ctx.beginPath();
|
|
173
|
-
ctx.arc(kp
|
|
213
|
+
ctx.arc(...at(kp), radius, 0, 2 * Math.PI);
|
|
174
214
|
ctx.fill();
|
|
175
215
|
}
|
|
176
216
|
}
|
|
177
|
-
}, [showSkeleton, showConfidence]);
|
|
217
|
+
}, [showSkeleton, showConfidence, width, height]);
|
|
218
|
+
// The detection loop is started once; it reaches this render's drawing and callbacks here.
|
|
219
|
+
const latestRef = useRef({ drawSkeleton, onFrame, onError });
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
latestRef.current = { drawSkeleton, onFrame, onError };
|
|
222
|
+
});
|
|
178
223
|
const handleFrame = useCallback((frame) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
var _a, _b;
|
|
225
|
+
latestRef.current.drawSkeleton(frame);
|
|
226
|
+
(_b = (_a = latestRef.current).onFrame) === null || _b === void 0 ? void 0 : _b.call(_a, frame);
|
|
227
|
+
}, []);
|
|
228
|
+
const handleDetectorError = useCallback((error) => {
|
|
229
|
+
var _a, _b;
|
|
230
|
+
(_b = (_a = latestRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
|
|
231
|
+
}, []);
|
|
182
232
|
const handleStart = useCallback(async () => {
|
|
183
233
|
try {
|
|
184
234
|
if (videoRef.current) {
|
|
185
|
-
await detection.start(videoRef.current, handleFrame);
|
|
235
|
+
await detection.start(videoRef.current, handleFrame, handleDetectorError);
|
|
186
236
|
onStart === null || onStart === void 0 ? void 0 : onStart();
|
|
187
237
|
}
|
|
188
238
|
}
|
|
189
239
|
catch (err) {
|
|
190
240
|
onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(String(err)));
|
|
191
241
|
}
|
|
192
|
-
}, [detection, handleFrame, onStart, onError]);
|
|
242
|
+
}, [detection, handleFrame, handleDetectorError, onStart, onError]);
|
|
193
243
|
const handleStop = useCallback(() => {
|
|
244
|
+
var _a;
|
|
194
245
|
detection.stop();
|
|
246
|
+
const canvas = canvasRef.current;
|
|
247
|
+
(_a = canvas === null || canvas === void 0 ? void 0 : canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.clearRect(0, 0, canvas.width, canvas.height);
|
|
195
248
|
onStop === null || onStop === void 0 ? void 0 : onStop();
|
|
196
249
|
}, [detection, onStop]);
|
|
197
250
|
const containerStyle = {
|
|
@@ -232,7 +285,5 @@ const NicePoseDetector = forwardRef(function NicePoseDetector(props, ref) {
|
|
|
232
285
|
fontWeight: 600,
|
|
233
286
|
}, children: "Start Camera" }))] }));
|
|
234
287
|
});
|
|
235
|
-
/** @deprecated Use NicePoseDetector */
|
|
236
|
-
const PoseDetector = NicePoseDetector;
|
|
237
288
|
|
|
238
|
-
export { NicePoseDetector,
|
|
289
|
+
export { NicePoseDetector, usePoseDetection };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { NiceModelEditor } from './model/ModelEditor.js';
|
|
2
|
+
export { NiceModelViewer } from './model/ModelViewer.js';
|
|
3
3
|
export { DEFAULT_CAD_SETTINGS, FLOOR_MATERIALS, ROOM_COLORS, UNIT_SCALES, WALL_MATERIALS, createCADGrid, snapToCADGrid } from './model/cadModeUtils.js';
|
|
4
4
|
export { FURNITURE_CATALOG, FURNITURE_CATEGORIES, ROOM_PRESETS, applyRoomLayout, calculateBudget, checkFurnitureCollision, createDragPreview, createFurnitureMesh, getFloorIntersection, placeFurnitureInScene, removeFurnitureFromScene, resizePlacedItem, rotatePlacedItem } from './model/simsModeUtils.js';
|
|
5
5
|
export { useModelEditor } from './model/useModelEditor.js';
|
|
@@ -7,7 +7,7 @@ export { useModelViewer } from './model/useModelViewer.js';
|
|
|
7
7
|
export { NiceI18nProvider, useNiceTranslation } from './core/i18n.js';
|
|
8
8
|
export { HDRI_PRESETS, PBR_PRESETS, QUAD_VIEWPORT, UndoRedoStack, applyHDRIPreset, applyPBRPreset, blendAnimations, computeSceneBounds, convertToInstanced, countTriangles, createAnimationClip, createMaterialFromPreset, createViewportCamera, crossfadeAnimations, downloadBlob, evaluateTrack, exportSceneGLTF, exportSceneUSDZ, generateLOD, makeAdditiveAction, measureAngle, measureDistance, removeKeyframe, renderMultiViewport, setKeyframe, snapMulti, snapToEdge, snapToGrid, snapToVertex, solveIKChain, subclip, toThreeKeyframeTrack } from './model/modelEditorUtils.js';
|
|
9
9
|
export { EditorShortcutManager } from './model/editorShortcuts.js';
|
|
10
|
-
export { NicePoseDetector,
|
|
10
|
+
export { NicePoseDetector, usePoseDetection } from './dance/PoseDetector.js';
|
|
11
11
|
export { DanceScoreEngine, useDanceScore } from './dance/DanceScoreEngine.js';
|
|
12
12
|
export { DanceBridge, rhythmToBeatSync, useDanceBridge } from './dance/DanceBridge.js';
|
|
13
13
|
export { NiceUVEditor } from './uv/NiceUVEditor.js';
|
|
@@ -29,7 +29,5 @@ const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
|
|
|
29
29
|
e.target.value = '';
|
|
30
30
|
} }), jsx("input", { ref: api.videoInputRef, type: "file", accept: "video/*", style: { display: 'none' }, onChange: api.handleAIVideo }), jsx(NiceAppShell, { className: `${styles.root} ${className !== null && className !== void 0 ? className : ''}`, flush: true, menubar: jsx(ModelEditorMenuBar, { api: api, hasOnSaveToLibrary: !!onSaveToLibrary }), sidebar: jsxs("div", { style: { display: 'flex', minHeight: 0 }, children: [jsx(ModelEditorLeftPanel, { api: api }), jsx(ModelEditorToolbar, { api: api })] }), rightSidebar: jsx(ModelEditorRightPanel, { api: api }), bottomPanel: jsx(ModelEditorTimeline, { api: api }), statusBar: jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("span", { style: { marginLeft: 'auto' }, children: [api.polyCount.toLocaleString(), " triangles \u00B7 ", api.meshCount, " meshes \u00B7 ", api.boneCount, ' ', "bones \u00B7 ", api.animations.length, " anim(s)"] })] }), children: jsx(ModelEditorViewport, { api: api }) })] }));
|
|
31
31
|
};
|
|
32
|
-
/** @deprecated Use NiceModelEditor */
|
|
33
|
-
const ModelEditor = NiceModelEditor;
|
|
34
32
|
|
|
35
|
-
export {
|
|
33
|
+
export { NiceModelEditor };
|
|
@@ -60,7 +60,5 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
|
|
|
60
60
|
a.click();
|
|
61
61
|
}, title: "Screenshot", "aria-label": "Screenshot", children: jsx(NiceIcon, { name: "camera", size: 16 }) }), jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: "Fullscreen", "aria-label": "Fullscreen", children: jsx(NiceIcon, { name: api.isFullscreen ? 'collapse' : 'expand', size: 16 }) }), toolbarExtra, showInfoPanel && (jsx("div", { className: styles.toolRight, children: jsx(NiceButton, { className: `${styles.toolBtn} ${panelOpen ? styles.active : ''}`, variant: panelOpen ? 'primary' : 'ghost', size: "sm", onClick: () => setPanelOpen(!panelOpen), title: "Info panel", "aria-label": "Info panel", children: jsx(NiceIcon, { name: "info", size: 16 }) }) }))] })), jsxs("div", { className: styles.viewport, ref: api.mountRef, tabIndex: 0, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [api.loading && (jsxs("div", { className: styles.loadingOverlay, children: [jsx("span", { className: styles.loadingText, children: "Loading model\u2026" }), jsx("div", { className: styles.loadingBar, children: jsx("div", { className: styles.loadingBarFill, style: { width: `${(api.loadProgress * 100).toFixed(0)}%` } }) })] })), api.error && (jsxs("div", { className: styles.errorOverlay, children: [jsx(NiceIcon, { name: "warning", size: 28 }), jsx("span", { style: { marginTop: 8 }, children: api.error })] })), dragOver && jsx("div", { className: styles.dropZone, children: "Drop 3D model file here" })] }), jsxs("div", { className: `${styles.sidePanel} ${!panelOpen ? styles.hidden : ''}`, children: [jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Model Info" }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Triangles" }), jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Meshes" }), jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxs(Fragment, { children: [jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Width" }), jsx("span", { children: api.dimensions.width })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Height" }), jsx("span", { children: api.dimensions.height })] }), jsxs("div", { className: styles.panelRow, children: [jsx("label", { children: "Depth" }), jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Navigation" }), navModes.map((m) => (jsx(NiceButton, { className: styles.toolBtn, variant: api.navigationMode === m ? 'primary' : 'ghost', size: "sm", onClick: () => api.setNavigationMode(m), children: jsx(NavLabel, { mode: m }) }, m)))] }), jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Auto Rotate" }), jsx(NiceSlider, { min: 0, max: 10, step: 0.5, value: api.autoRotateSpeed, onChange: (val) => api.setAutoRotateSpeed(val), style: { width: '100%' } })] }), api.animations.length > 0 && (jsxs("div", { className: styles.panelSection, children: [jsx("div", { className: styles.panelTitle, children: "Animations" }), api.animations.map((name, i) => (jsxs(NiceButton, { className: styles.toolBtn, variant: api.activeAnimIdx === i ? 'primary' : 'ghost', size: "sm", onClick: () => api.playAnimation(i), children: [jsx(NiceIcon, { name: "play", size: 14 }), " ", name] }, i)))] }))] }), jsxs("div", { className: `${styles.animBar} ${api.animations.length === 0 ? styles.hidden : ''}`, children: [jsx(NiceButton, { className: styles.toolBtn, variant: "ghost", size: "sm", onClick: api.togglePlay, "aria-label": "Play/Pause", children: jsx(NiceIcon, { name: api.isPlaying ? 'pause' : 'play', size: 16 }) }), jsx(NiceSelect, { className: styles.animSelect, value: String(api.activeAnimIdx), onChange: (val) => api.playAnimation(parseInt(val)), options: api.animations.map((n, i) => ({ value: String(i), label: n })) }), jsx(NiceSlider, { className: styles.animSlider, min: 0, max: api.animDuration || 1, step: 0.01, value: api.animTime, onChange: (val) => api.seekAnim(val) }), jsxs("span", { className: styles.toolLabel, children: [api.animTime.toFixed(1), "s / ", api.animDuration.toFixed(1), "s"] })] }), showStatusBar && (jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: api.statusText }), jsxs("div", { className: styles.statusRight, children: [jsx("span", { children: jsx(NavLabel, { mode: api.navigationMode }) }), api.dimensions && (jsxs("span", { children: [api.dimensions.width, "\u00D7", api.dimensions.height, "\u00D7", api.dimensions.depth] }))] })] })), children === null || children === void 0 ? void 0 : children(api)] }));
|
|
62
62
|
};
|
|
63
|
-
/** @deprecated Use NiceModelViewer */
|
|
64
|
-
const ModelViewer = NiceModelViewer;
|
|
65
63
|
|
|
66
|
-
export {
|
|
64
|
+
export { NiceModelViewer };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { NiceTutorialButton, resolveTutorial, NiceIcon } from '@nice2dev/ui';
|
|
2
|
+
import { NiceTutorialButton, resolveTutorial, NiceIcon, createId } from '@nice2dev/ui';
|
|
3
3
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
|
|
@@ -145,7 +145,7 @@ function useArmatureEditor(skeleton, scene, onBoneSelect) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
const pose = {
|
|
148
|
-
id:
|
|
148
|
+
id: createId(),
|
|
149
149
|
name,
|
|
150
150
|
timestamp: Date.now(),
|
|
151
151
|
boneTransforms: transforms,
|
|
@@ -222,7 +222,7 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
|
|
|
222
222
|
padding: '4px 8px',
|
|
223
223
|
paddingLeft: depth * 16 + 8,
|
|
224
224
|
background: isSelected ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
225
|
-
color: isSelected ? 'var(--nice-
|
|
225
|
+
color: isSelected ? 'var(--nice-color-white, #fff)' : 'inherit',
|
|
226
226
|
cursor: 'pointer',
|
|
227
227
|
fontSize: 13,
|
|
228
228
|
borderRadius: 4,
|
|
@@ -268,7 +268,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
|
|
|
268
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"] }), jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), "Debug"] })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
|
|
269
269
|
flex: 1,
|
|
270
270
|
background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
|
|
271
|
-
color: 'var(--nice-
|
|
271
|
+
color: 'var(--nice-color-white, #fff)',
|
|
272
272
|
border: 'none',
|
|
273
273
|
borderRadius: 4,
|
|
274
274
|
padding: '5px 8px',
|
|
@@ -343,12 +343,12 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
|
|
|
343
343
|
borderRadius: 4,
|
|
344
344
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
345
345
|
background: brush.mode === m ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
346
|
-
color: brush.mode === m ? 'var(--nice-
|
|
346
|
+
color: brush.mode === m ? 'var(--nice-color-white, #fff)' : 'inherit',
|
|
347
347
|
}, children: m }, m)))] }), jsxs("label", { children: ["Radius: ", brush.radius.toFixed(1), jsx("input", { type: "range", min: 0.5, max: 20, step: 0.5, value: brush.radius, onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: ["Strength: ", brush.strength.toFixed(2), jsx("input", { type: "range", min: 0.01, max: 1, step: 0.01, value: brush.strength, onChange: (e) => setBrush({ strength: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsx("button", { type: "button", onClick: isPainting ? stopPainting : startPainting, style: {
|
|
348
348
|
background: isPainting
|
|
349
349
|
? 'var(--nice-danger, #ef4444)'
|
|
350
350
|
: 'var(--nice-color-accent, #6366f1)',
|
|
351
|
-
color: 'var(--nice-
|
|
351
|
+
color: 'var(--nice-color-white, #fff)',
|
|
352
352
|
border: 'none',
|
|
353
353
|
borderRadius: 4,
|
|
354
354
|
padding: '6px 12px',
|
|
@@ -239,7 +239,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
|
|
|
239
239
|
flex: 1,
|
|
240
240
|
padding: '4px 6px',
|
|
241
241
|
background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
242
|
-
color: brush.mode === mode ? 'var(--nice-
|
|
242
|
+
color: brush.mode === mode ? 'var(--nice-color-white, #fff)' : 'inherit',
|
|
243
243
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
244
244
|
borderRadius: 4,
|
|
245
245
|
cursor: 'pointer',
|
|
@@ -249,7 +249,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
|
|
|
249
249
|
background: isPainting
|
|
250
250
|
? 'var(--nice-danger, #ef4444)'
|
|
251
251
|
: 'var(--nice-color-accent, #6366f1)',
|
|
252
|
-
color: 'var(--nice-
|
|
252
|
+
color: 'var(--nice-color-white, #fff)',
|
|
253
253
|
border: 'none',
|
|
254
254
|
borderRadius: 4,
|
|
255
255
|
padding: '6px 12px',
|
|
@@ -271,7 +271,7 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
|
|
|
271
271
|
borderRadius: 4,
|
|
272
272
|
cursor: 'pointer',
|
|
273
273
|
background: mode === 'vr' ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
274
|
-
color: mode === 'vr' ? 'var(--nice-
|
|
274
|
+
color: mode === 'vr' ? 'var(--nice-color-white, #fff)' : 'inherit',
|
|
275
275
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
276
276
|
}, children: [jsx(NiceIcon, { name: "eye", size: 14 }), " VR"] }), jsxs("button", { type: "button", onClick: () => setMode('ar'), style: {
|
|
277
277
|
flex: 1,
|
|
@@ -279,13 +279,13 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
|
|
|
279
279
|
borderRadius: 4,
|
|
280
280
|
cursor: 'pointer',
|
|
281
281
|
background: mode === 'ar' ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
|
|
282
|
-
color: mode === 'ar' ? 'var(--nice-
|
|
282
|
+
color: mode === 'ar' ? 'var(--nice-color-white, #fff)' : 'inherit',
|
|
283
283
|
border: '1px solid var(--nice-border, #e2e8f0)',
|
|
284
284
|
}, children: [jsx(NiceIcon, { name: "window", size: 14 }), " AR"] })] }), jsx("button", { type: "button", onClick: isSessionActive ? endSession : startSession, style: {
|
|
285
285
|
background: isSessionActive
|
|
286
286
|
? 'var(--nice-danger, #ef4444)'
|
|
287
287
|
: 'var(--nice-success, #22c55e)',
|
|
288
|
-
color: 'var(--nice-
|
|
288
|
+
color: 'var(--nice-color-white, #fff)',
|
|
289
289
|
border: 'none',
|
|
290
290
|
borderRadius: 4,
|
|
291
291
|
padding: '6px 12px',
|