@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +11 -15
  3. package/dist/cjs/core/threeCompat.js +52 -0
  4. package/dist/cjs/dance/PoseDetector.js +112 -62
  5. package/dist/cjs/index.js +0 -3
  6. package/dist/cjs/model/ModelEditor.js +0 -3
  7. package/dist/cjs/model/ModelViewer.js +0 -3
  8. package/dist/cjs/model/NiceArmatureEditor.js +2 -2
  9. package/dist/cjs/model/NicePhysicsSimulation.js +1 -1
  10. package/dist/cjs/model/NiceTerrainEditor.js +2 -2
  11. package/dist/cjs/model/NiceWeightPainter.js +2 -2
  12. package/dist/cjs/model/NiceXRPreview.js +3 -3
  13. package/dist/cjs/model/useModelEditor.js +8 -3
  14. package/dist/cjs/model/useModelViewer.js +13 -9
  15. package/dist/cjs/rendering/NiceRenderExport.js +2 -2
  16. package/dist/cjs/rendering/NiceWebGPURenderer.js +3 -2
  17. package/dist/esm/core/threeCompat.js +30 -0
  18. package/dist/esm/dance/PoseDetector.js +114 -63
  19. package/dist/esm/index.js +3 -3
  20. package/dist/esm/model/ModelEditor.js +1 -3
  21. package/dist/esm/model/ModelViewer.js +1 -3
  22. package/dist/esm/model/NiceArmatureEditor.js +3 -3
  23. package/dist/esm/model/NicePhysicsSimulation.js +1 -1
  24. package/dist/esm/model/NiceTerrainEditor.js +2 -2
  25. package/dist/esm/model/NiceWeightPainter.js +2 -2
  26. package/dist/esm/model/NiceXRPreview.js +3 -3
  27. package/dist/esm/model/useModelEditor.js +8 -3
  28. package/dist/esm/model/useModelViewer.js +13 -9
  29. package/dist/esm/rendering/NiceRenderExport.js +2 -2
  30. package/dist/esm/rendering/NiceWebGPURenderer.js +3 -2
  31. package/dist/types/core/threeCompat.d.ts +14 -0
  32. package/dist/types/dance/PoseDetector.d.ts +14 -13
  33. package/dist/types/index.d.ts +4 -4
  34. package/dist/types/model/ModelEditor.d.ts +0 -4
  35. package/dist/types/model/ModelViewer.d.ts +0 -4
  36. 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
- const clockRef = React.useRef(new THREE__namespace.Clock());
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 = THREE__namespace.PCFSoftShadowMap;
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
- const clockRef = React.useRef(new THREE__namespace.Clock());
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 = THREE__namespace.PCFSoftShadowMap;
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 = (_a = navigator.getGamepads) === null || _a === void 0 ? void 0 : _a.call(navigator);
189
- const gp = (_b = pads === null || pads === void 0 ? void 0 : pads[0]) !== null && _b !== void 0 ? _b : null;
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 = (_c = gp.axes[0]) !== null && _c !== void 0 ? _c : 0;
192
- const ly = (_d = gp.axes[1]) !== null && _d !== void 0 ? _d : 0;
193
- const rx = (_e = gp.axes[2]) !== null && _e !== void 0 ? _e : 0;
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-text-on-primary, #fff)'
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-text-on-primary, #fff)',
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 = THREE__namespace.PCFSoftShadowMap;
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-text-on-primary, #fff)',
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, useState, useEffect } from 'react';
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
- // ── Simulated pose detection (no external dependency) ───────────
44
- function createSimulatedKeypoints() {
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
- const { maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = config;
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 processFrame = useCallback(() => {
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
- if (!videoRef.current || videoRef.current.readyState < 2) {
66
- rafRef.current = requestAnimationFrame(processFrame);
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 idx = frameIndexRef.current++;
70
- const timestamp = idx / targetFps;
71
- // Build frame from video analysis (placeholder for actual ML model)
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
- const frame = {
81
- frame_index: idx,
82
- timestamp_sec: timestamp,
83
- persons,
84
- };
85
- latestFrameRef.current = frame;
86
- (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
87
- rafRef.current = requestAnimationFrame(processFrame);
88
- }, [maxPersons, minConfidence, targetFps]);
89
- const start = useCallback(async (video, onFrame) => {
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, _b, _c, _d, _e, _f;
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 = kps[a], kb = kps[b];
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 (((_b = ka.confidence) !== null && _b !== void 0 ? _b : 0) < 0.3 || ((_c = kb.confidence) !== null && _c !== void 0 ? _c : 0) < 0.3) {
194
+ if (confidenceOf(ka) < 0.3 || confidenceOf(kb) < 0.3) {
155
195
  continue;
156
196
  }
157
197
  ctx.beginPath();
158
- ctx.moveTo(ka.x, ka.y);
159
- ctx.lineTo(kb.x, kb.y);
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 (((_d = kp.confidence) !== null && _d !== void 0 ? _d : 0) < 0.3) {
205
+ if (confidenceOf(kp) < 0.3) {
166
206
  continue;
167
207
  }
168
- const radius = showConfidence ? 3 + ((_e = kp.confidence) !== null && _e !== void 0 ? _e : 0) * 5 : 4;
208
+ const radius = showConfidence ? 3 + confidenceOf(kp) * 5 : 4;
169
209
  ctx.fillStyle = resolveCssColor(showConfidence
170
- ? `rgba(0, 255, 136, ${(_f = kp.confidence) !== null && _f !== void 0 ? _f : 0.5})`
210
+ ? `rgba(0, 255, 136, ${confidenceOf(kp)})`
171
211
  : 'var(--nice-success, #00ff88)', ctx.canvas);
172
212
  ctx.beginPath();
173
- ctx.arc(kp.x, kp.y, radius, 0, 2 * Math.PI);
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
- drawSkeleton(frame);
180
- onFrame === null || onFrame === void 0 ? void 0 : onFrame(frame);
181
- }, [drawSkeleton, onFrame]);
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, PoseDetector, usePoseDetection };
289
+ export { NicePoseDetector, usePoseDetection };
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { ModelEditor, NiceModelEditor } from './model/ModelEditor.js';
2
- export { ModelViewer, NiceModelViewer } from './model/ModelViewer.js';
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, PoseDetector, usePoseDetection } from './dance/PoseDetector.js';
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 { ModelEditor, NiceModelEditor };
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 { ModelViewer, NiceModelViewer };
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: crypto.randomUUID(),
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-text-on-primary, #fff)' : 'inherit',
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-text-on-primary, #fff)',
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-text-on-primary, #fff)' : 'inherit',
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-text-on-primary, #fff)',
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-text-on-primary, #fff)' : 'inherit',
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-text-on-primary, #fff)',
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-text-on-primary, #fff)' : 'inherit',
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-text-on-primary, #fff)' : 'inherit',
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-text-on-primary, #fff)',
288
+ color: 'var(--nice-color-white, #fff)',
289
289
  border: 'none',
290
290
  borderRadius: 4,
291
291
  padding: '6px 12px',