@nice2dev/ui-3d 1.1.9 → 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 +76 -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 +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # @nice2dev/ui-3d — Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3a4939d: 1.2.0 — the API cleanup announced for this version, dictionaries per language by default, and
8
+ three new controls. **Breaking:** every removal has a replacement, listed name by name in
9
+ `documentation/omniverk/OMNIVERK_UPGRADE_1.2.0.md`.
10
+
11
+ **Removed (breaking).** All 118 `@deprecated` names: the aliases without the `Nice` prefix in
12
+ `ui-audio`, `ui-graphics` and `ui-3d`; `NiceBarcodeGenerator` (→ `NiceBarcode`), `NiceFloorPlanCanvas`
13
+ (→ `NiceSpatialEditor` in `@nice2dev/ui-buildings`), the v2 props and types of `NiceTopNav`,
14
+ `NiceProgress.showLabel`, `NiceCountryPicker.showDialCode` / `showFlag`, `type` on
15
+ `NiceDateTimePicker` / `NiceDateRangeBox` (→ `dateFormatPreset`); `ZoneOccupancy` in
16
+ `ui-buildings` and `events`, the `SmartHome*` types; the icon variant `flat` (→ `cutout`) and
17
+ `NiceIconEditor` (→ `NiceVectorEditor`); `NiceSizeLegacy`; the `snapGrid` module of `ui-diagrams` (→
18
+ `@nice2dev/spatial-core`); the opt-in stylesheet `@nice2dev/ui/css/legacy-bg-text-aliases.css`, which
19
+ formed an alias cycle with `style.css`. Props that were accepted and never read — 19 on
20
+ `NiceModuleGrid`, and more on `NiceAutocomplete`, `NiceTaskInbox`, `NiceConsentBanner`, the
21
+ building and communication controls, four `ui-audio` panels and `NiceStreamDashboard` — are gone
22
+ from the types. `NiceResourceAllocation` in `ui-erp` is `NiceCapacityPlanner` (the one in `ui`
23
+ keeps its name); `NiceGenericPlayerStageVisualizer` (→ `NiceStageVisualizer`) and
24
+ `NiceCubeBrowserProps` are removed. The `@nice2dev/ui/overlays` entry calls its modal, drawer,
25
+ bottom sheet and context menu `NiceOverlayModal`, `NiceOverlayDrawer`, `NiceOverlayBottomSheet` and
26
+ `NiceOverlayContextMenu`: the old names belonged to different components of `@nice2dev/ui`.
27
+
28
+ **Dictionaries per language.** `NiceI18nProvider` loads only the language in use, with `import()`
29
+ (it was `NiceI18nLazyProvider` in 1.1.10, which stays as the same component); the controls show their
30
+ built-in English until it arrives — `await loadNiceDictionary(lang)` before the first render to start
31
+ in the language. Nothing on the main entry of `@nice2dev/i18n` imports a dictionary any more:
32
+ `NICE_I18N_DICTIONARIES`, `createTranslator`, `NICE_I18N_VARIANTS` / `resolvePlVariant` and the
33
+ translation tooling moved to the new entry `@nice2dev/i18n/all`, and `@nice2dev/ui-core` no longer
34
+ re-exports the dictionary map. `@nice2dev/ui` with what it bundles: 1283 kB → 952 kB gzip loaded up
35
+ front. The `ui-audio` controls read the app's `NiceI18nProvider` now; their own provider overrides it.
36
+
37
+ **New controls.** `NiceRecurrenceEditor`, `NiceStopwatch`, `NiceCountdownTimer` (`@nice2dev/ui`) and
38
+ `NicePasswordGenerator` (`@nice2dev/auth`), plus `recurrenceDates()` and `generatePassword()`.
39
+ `NiceScheduler` honours a weekly rule's `byDay`, which it ignored.
40
+
41
+ **Visible changes.** The palette meets WCAG AA for text on every fill: darker fills in the light
42
+ theme, dark text on the fills of the dark theme (new `--nice-text-on-success` / `-error` / `-info` /
43
+ `-accent` / `-warning`; `NiceThemeText` takes `onSuccess`, `onError`, `onInfo`, `onWarning`). Mobile
44
+ and platform panels follow the app theme instead of `prefers-color-scheme`. Swipes in
45
+ `NiceSwipeActions`, `NiceTouchTable` and `NiceMobileCalendar` work with a mouse and a pen. The
46
+ `ui-forms` editors have the intended spacing (it was four times too large). `NiceMAM`,
47
+ `NiceTranscoder`, `NiceVideoAnnotator` and `NiceStreamDashboard` use `nice-`-prefixed BEM classes.
48
+
49
+ **Fixes.** Controls that invented results ask the host instead: `NiceIntegrationHub`,
50
+ `NiceMLStudio` (`onExperimentRun` may return metrics), `NiceRemoteManagement`
51
+ (`onCaptureScreenshot`), `NicePoseDetector` (`detector`), `NiceBatchScannerProvider`,
52
+ `NiceCacheStrategies`; `NiceLowCodeStudio` accepts dropped components. Controls that did nothing
53
+ work or say why they are off: `NiceStreamDashboard` adds sources, sends chat and runs the chosen
54
+ transition (new `onAddPlatform`, `onSendChatMessage`, `onTransition`, `onScreenshot`,
55
+ `onOpenAudioMixer`, `onOpenSettings`; `receiveChatMessage` feeds incoming chat), `NiceMLStudio`
56
+ downloads through `onModelDownload`, `NiceETLBuilder` saves through `onSave` (its Save button had
57
+ no handler), `NiceTranscoder` makes jobs of picked and dropped files, `NiceAutomationEditor` offers
58
+ the trigger types its model has. Texts of about fifty components go
59
+ through `t()` in six languages, among them the AI data grid, chart and form builders, the ML
60
+ studio, the report designer, the song picker, `NiceProtocolConfig`, `NiceETLBuilder` and
61
+ `NiceInlineHelp` — every key the controls pass to `t()` now has a dictionary entry, so none of
62
+ them stays English under another language; `NiceDataGridAI` sorts on a query in any language, not
63
+ only in English. `NiceWindow`, `NiceConsole`, `NiceFileManager` and `NiceGitManager` no longer
64
+ crash when `accessMode` switches to or from `'hidden'`. Ids come from `createId()` (`@nice2dev/ui-core`), so nothing throws
65
+ outside a secure context as `crypto.randomUUID()` did on plain http. `ui-3d` runs on three r183
66
+ without its deprecation warnings. `NiceRemoteManagement` calls the handlers of the latest render.
67
+ The AI hint popover and the `ui-platform` panels follow the app theme, not the system's or a
68
+ `.dark` class on the host. `NiceCommandPalette` has styles and its search filters;
69
+ `NiceTimeoutFeedback` starts its clock; `NiceOfflineBanner` takes `online`; Monaco's workers in
70
+ `NiceCodeEditor` load; the `ui-audio` panels that used Bootstrap classes use Nice controls; colours
71
+ that reached a canvas as `var(…)` (karaoke lanes and notes) are resolved first.
72
+
73
+ ## 1.1.10
74
+
75
+ ### Patch Changes
76
+
77
+ - The package description on npm shows a dash instead of the mis-encoded "—".
78
+
3
79
  ## 1.1.9
4
80
 
5
81
  ### Patch Changes
package/README.md CHANGED
@@ -27,22 +27,18 @@ npm install @nice2dev/ui-3d three
27
27
  ## Quick start
28
28
 
29
29
  ```tsx
30
- import { ModelEditor } from '@nice2dev/ui-3d';
30
+ import { NiceModelEditor } from '@nice2dev/ui-3d';
31
31
  import '@nice2dev/ui-3d/style.css';
32
32
 
33
33
  function App() {
34
- return (
35
- <ModelEditor
36
- onSaveToLibrary={(dataUrl, name, mimeType) => console.log('save', name)}
37
- />
38
- );
34
+ return <NiceModelEditor onSaveToLibrary={(dataUrl, name, mimeType) => console.log('save', name)} />;
39
35
  }
40
36
  ```
41
37
 
42
38
  ## With AI pose-from-video
43
39
 
44
40
  ```tsx
45
- import { ModelEditor, type Pose3DSequenceResult } from '@nice2dev/ui-3d';
41
+ import { NiceModelEditor, type Pose3DSequenceResult } from '@nice2dev/ui-3d';
46
42
 
47
43
  async function myAiPose(file: File): Promise<Pose3DSequenceResult> {
48
44
  const form = new FormData();
@@ -51,20 +47,20 @@ async function myAiPose(file: File): Promise<Pose3DSequenceResult> {
51
47
  return res.json();
52
48
  }
53
49
 
54
- <ModelEditor onAIPoseVideo={myAiPose} />
50
+ <NiceModelEditor onAIPoseVideo={myAiPose} />;
55
51
  ```
56
52
 
57
53
  ## With i18n
58
54
 
59
55
  ```tsx
60
- import { ModelEditor, NiceI18nProvider } from '@nice2dev/ui-3d';
56
+ import { NiceModelEditor, NiceI18nProvider } from '@nice2dev/ui-3d';
61
57
  import { useTranslation } from 'react-i18next';
62
58
 
63
59
  function App() {
64
60
  const { t } = useTranslation();
65
61
  return (
66
62
  <NiceI18nProvider t={(key, fallback) => t(key, fallback)}>
67
- <ModelEditor />
63
+ <NiceModelEditor />
68
64
  </NiceI18nProvider>
69
65
  );
70
66
  }
@@ -72,11 +68,11 @@ function App() {
72
68
 
73
69
  ## Props
74
70
 
75
- | Prop | Type | Default | Description |
76
- |------|------|---------|-------------|
77
- | `className` | `string` | — | CSS class added to root element |
78
- | `onSaveToLibrary` | `(dataUrl, name, mimeType) => void` | — | Called when user saves render |
79
- | `onAIPoseVideo` | `(file: File) => Promise<Pose3DSequenceResult>` | — | AI pose callback; button hidden if omitted |
71
+ | Prop | Type | Default | Description |
72
+ | ----------------- | ----------------------------------------------- | ------- | ------------------------------------------ |
73
+ | `className` | `string` | — | CSS class added to root element |
74
+ | `onSaveToLibrary` | `(dataUrl, name, mimeType) => void` | — | Called when user saves render |
75
+ | `onAIPoseVideo` | `(file: File) => Promise<Pose3DSequenceResult>` | — | AI pose callback; button hidden if omitted |
80
76
 
81
77
  ## Build
82
78
 
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+
5
+ function _interopNamespaceDefault(e) {
6
+ var n = Object.create(null);
7
+ if (e) {
8
+ Object.keys(e).forEach(function (k) {
9
+ if (k !== 'default') {
10
+ var d = Object.getOwnPropertyDescriptor(e, k);
11
+ Object.defineProperty(n, k, d.get ? d : {
12
+ enumerable: true,
13
+ get: function () { return e[k]; }
14
+ });
15
+ }
16
+ });
17
+ }
18
+ n.default = e;
19
+ return Object.freeze(n);
20
+ }
21
+
22
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
23
+
24
+ /**
25
+ * three.js r183 deprecated `Clock` (for `Timer`) and folds `PCFSoftShadowMap` into
26
+ * `PCFShadowMap`; both log a console warning there. The peer range starts at r150, which has
27
+ * neither `Timer` in its core nor the softer `PCFShadowMap`, so the choice follows the revision.
28
+ */
29
+ const REVISION = Number.parseInt(THREE__namespace.REVISION, 10);
30
+ const MODERN = REVISION >= 183;
31
+ /** Soft shadows without the r183 deprecation warning; older three keeps `PCFSoftShadowMap`. */
32
+ const SOFT_SHADOW_MAP = MODERN
33
+ ? THREE__namespace.PCFShadowMap
34
+ : THREE__namespace.PCFSoftShadowMap;
35
+ /** `Timer` where three has deprecated `Clock`, `Clock` before. */
36
+ function createFrameClock() {
37
+ const TimerCtor = THREE__namespace.Timer;
38
+ if (MODERN && TimerCtor) {
39
+ const timer = new TimerCtor();
40
+ return {
41
+ getDelta: () => {
42
+ timer.update();
43
+ return timer.getDelta();
44
+ },
45
+ };
46
+ }
47
+ const clock = new THREE__namespace.Clock();
48
+ return { getDelta: () => clock.getDelta() };
49
+ }
50
+
51
+ exports.SOFT_SHADOW_MAP = SOFT_SHADOW_MAP;
52
+ exports.createFrameClock = createFrameClock;
@@ -42,19 +42,15 @@ const SKELETON_CONNECTIONS = [
42
42
  [12, 14],
43
43
  [14, 16], // right leg
44
44
  ];
45
- // ── Simulated pose detection (no external dependency) ───────────
46
- function createSimulatedKeypoints() {
47
- return COCO_KEYPOINTS.map((name, _i) => ({
48
- name,
49
- x: 0,
50
- y: 0,
51
- z: 0,
52
- confidence: 0,
53
- }));
54
- }
45
+ /** A keypoint the detector reported without a score counts as found. */
46
+ const confidenceOf = (kp) => { var _a; return (_a = kp.confidence) !== null && _a !== void 0 ? _a : 1; };
55
47
  // ── Hook: usePoseDetection ──────────────────────────────────────
56
48
  function usePoseDetection(config = {}) {
57
- const { maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = config;
49
+ // The detection loop outlives renders: it reads the latest settings from here.
50
+ const configRef = React.useRef(config);
51
+ React.useEffect(() => {
52
+ configRef.current = config;
53
+ });
58
54
  const [isDetecting, setIsDetecting] = React.useState(false);
59
55
  const latestFrameRef = React.useRef(null);
60
56
  const frameIndexRef = React.useRef(0);
@@ -62,33 +58,67 @@ function usePoseDetection(config = {}) {
62
58
  const streamRef = React.useRef(null);
63
59
  const videoRef = React.useRef(null);
64
60
  const callbackRef = React.useRef(null);
65
- const processFrame = React.useCallback(() => {
61
+ const errorRef = React.useRef(null);
62
+ const startedAtRef = React.useRef(0);
63
+ const lastRunRef = React.useRef(-Infinity);
64
+ const busyRef = React.useRef(false);
65
+ // Bumped by start() and stop(): a detection that resolves after them is dropped.
66
+ const runRef = React.useRef(0);
67
+ const stop = React.useCallback(() => {
66
68
  var _a;
67
- if (!videoRef.current || videoRef.current.readyState < 2) {
68
- rafRef.current = requestAnimationFrame(processFrame);
69
+ runRef.current += 1;
70
+ cancelAnimationFrame(rafRef.current);
71
+ (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
72
+ streamRef.current = null;
73
+ if (videoRef.current) {
74
+ videoRef.current.srcObject = null;
75
+ }
76
+ setIsDetecting(false);
77
+ }, []);
78
+ const processFrame = React.useCallback(() => {
79
+ rafRef.current = requestAnimationFrame(processFrame);
80
+ const video = videoRef.current;
81
+ const { detector, maxPersons = 1, minConfidence = 0.5, targetFps = 30 } = configRef.current;
82
+ if (!detector || !video || video.readyState < 2 || busyRef.current) {
69
83
  return;
70
84
  }
71
- const idx = frameIndexRef.current++;
72
- const timestamp = idx / targetFps;
73
- // Build frame from video analysis (placeholder for actual ML model)
74
- // In production, this calls MediaPipe/TensorFlow.js pose estimation
75
- const persons = [];
76
- for (let p = 0; p < maxPersons; p++) {
77
- const keypoints = createSimulatedKeypoints();
78
- if (keypoints.some((kp) => { var _a; return ((_a = kp.confidence) !== null && _a !== void 0 ? _a : 0) >= minConfidence; })) {
79
- persons.push({ id: p, keypoints });
80
- }
85
+ const now = performance.now();
86
+ if (now - lastRunRef.current < 1000 / targetFps) {
87
+ return;
81
88
  }
82
- const frame = {
83
- frame_index: idx,
84
- timestamp_sec: timestamp,
85
- persons,
86
- };
87
- latestFrameRef.current = frame;
88
- (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
89
- rafRef.current = requestAnimationFrame(processFrame);
90
- }, [maxPersons, minConfidence, targetFps]);
91
- const start = React.useCallback(async (video, onFrame) => {
89
+ lastRunRef.current = now;
90
+ busyRef.current = true;
91
+ const run = runRef.current;
92
+ Promise.resolve()
93
+ .then(() => detector(video, now))
94
+ .then((found) => {
95
+ var _a;
96
+ if (run !== runRef.current) {
97
+ return;
98
+ }
99
+ const frame = {
100
+ frame_index: frameIndexRef.current++,
101
+ timestamp_sec: (now - startedAtRef.current) / 1000,
102
+ persons: found
103
+ .filter((person) => person.keypoints.some((kp) => confidenceOf(kp) >= minConfidence))
104
+ .slice(0, maxPersons),
105
+ };
106
+ latestFrameRef.current = frame;
107
+ (_a = callbackRef.current) === null || _a === void 0 ? void 0 : _a.call(callbackRef, frame);
108
+ })
109
+ .catch((err) => {
110
+ var _a;
111
+ if (run !== runRef.current) {
112
+ return;
113
+ }
114
+ stop();
115
+ (_a = errorRef.current) === null || _a === void 0 ? void 0 : _a.call(errorRef, err instanceof Error ? err : new Error(String(err)));
116
+ })
117
+ .finally(() => {
118
+ busyRef.current = false;
119
+ });
120
+ }, [stop]);
121
+ const start = React.useCallback(async (video, onFrame, onError) => {
92
122
  try {
93
123
  const stream = await navigator.mediaDevices.getUserMedia({
94
124
  video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } },
@@ -98,7 +128,14 @@ function usePoseDetection(config = {}) {
98
128
  video.srcObject = stream;
99
129
  await video.play();
100
130
  callbackRef.current = onFrame !== null && onFrame !== void 0 ? onFrame : null;
131
+ errorRef.current = onError !== null && onError !== void 0 ? onError : null;
101
132
  frameIndexRef.current = 0;
133
+ startedAtRef.current = performance.now();
134
+ lastRunRef.current = -Infinity;
135
+ runRef.current += 1;
136
+ if (!configRef.current.detector) {
137
+ console.warn('[NicePoseDetector] started without a `detector`: the camera runs, but no poses are detected.');
138
+ }
102
139
  setIsDetecting(true);
103
140
  rafRef.current = requestAnimationFrame(processFrame);
104
141
  }
@@ -106,16 +143,6 @@ function usePoseDetection(config = {}) {
106
143
  throw new Error(`Camera access failed: ${err instanceof Error ? err.message : String(err)}`);
107
144
  }
108
145
  }, [processFrame]);
109
- const stop = React.useCallback(() => {
110
- var _a;
111
- cancelAnimationFrame(rafRef.current);
112
- (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((t) => t.stop());
113
- streamRef.current = null;
114
- if (videoRef.current) {
115
- videoRef.current.srcObject = null;
116
- }
117
- setIsDetecting(false);
118
- }, []);
119
146
  React.useEffect(() => () => {
120
147
  stop();
121
148
  }, [stop]);
@@ -136,64 +163,90 @@ const NicePoseDetector = React.forwardRef(function NicePoseDetector(props, ref)
136
163
  },
137
164
  }));
138
165
  const drawSkeleton = React.useCallback((frame) => {
139
- var _a, _b, _c, _d, _e, _f;
166
+ var _a;
140
167
  const ctx = (_a = canvasRef.current) === null || _a === void 0 ? void 0 : _a.getContext('2d');
141
168
  if (!ctx || !canvasRef.current) {
142
169
  return;
143
170
  }
144
171
  ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
172
+ // Keypoints are in video pixels; the video fills the box with object-fit: cover.
173
+ const video = videoRef.current;
174
+ const videoW = (video === null || video === void 0 ? void 0 : video.videoWidth) || width;
175
+ const videoH = (video === null || video === void 0 ? void 0 : video.videoHeight) || height;
176
+ const scale = Math.max(width / videoW, height / videoH);
177
+ const offsetX = (width - videoW * scale) / 2;
178
+ const offsetY = (height - videoH * scale) / 2;
179
+ const at = (kp) => [
180
+ kp.x * scale + offsetX,
181
+ kp.y * scale + offsetY,
182
+ ];
145
183
  for (const person of frame.persons) {
146
184
  const kps = person.keypoints;
185
+ const byName = new Map(kps.map((kp) => [kp.name, kp]));
147
186
  // Draw connections
148
187
  if (showSkeleton) {
149
188
  ctx.strokeStyle = ui.resolveCssColor('var(--nice-success, #00ff88)', ctx.canvas);
150
189
  ctx.lineWidth = 2;
151
190
  for (const [a, b] of SKELETON_CONNECTIONS) {
152
- const ka = kps[a], kb = kps[b];
191
+ const ka = byName.get(COCO_KEYPOINTS[a]);
192
+ const kb = byName.get(COCO_KEYPOINTS[b]);
153
193
  if (!ka || !kb) {
154
194
  continue;
155
195
  }
156
- if (((_b = ka.confidence) !== null && _b !== void 0 ? _b : 0) < 0.3 || ((_c = kb.confidence) !== null && _c !== void 0 ? _c : 0) < 0.3) {
196
+ if (confidenceOf(ka) < 0.3 || confidenceOf(kb) < 0.3) {
157
197
  continue;
158
198
  }
159
199
  ctx.beginPath();
160
- ctx.moveTo(ka.x, ka.y);
161
- ctx.lineTo(kb.x, kb.y);
200
+ ctx.moveTo(...at(ka));
201
+ ctx.lineTo(...at(kb));
162
202
  ctx.stroke();
163
203
  }
164
204
  }
165
205
  // Draw keypoints
166
206
  for (const kp of kps) {
167
- if (((_d = kp.confidence) !== null && _d !== void 0 ? _d : 0) < 0.3) {
207
+ if (confidenceOf(kp) < 0.3) {
168
208
  continue;
169
209
  }
170
- const radius = showConfidence ? 3 + ((_e = kp.confidence) !== null && _e !== void 0 ? _e : 0) * 5 : 4;
210
+ const radius = showConfidence ? 3 + confidenceOf(kp) * 5 : 4;
171
211
  ctx.fillStyle = ui.resolveCssColor(showConfidence
172
- ? `rgba(0, 255, 136, ${(_f = kp.confidence) !== null && _f !== void 0 ? _f : 0.5})`
212
+ ? `rgba(0, 255, 136, ${confidenceOf(kp)})`
173
213
  : 'var(--nice-success, #00ff88)', ctx.canvas);
174
214
  ctx.beginPath();
175
- ctx.arc(kp.x, kp.y, radius, 0, 2 * Math.PI);
215
+ ctx.arc(...at(kp), radius, 0, 2 * Math.PI);
176
216
  ctx.fill();
177
217
  }
178
218
  }
179
- }, [showSkeleton, showConfidence]);
219
+ }, [showSkeleton, showConfidence, width, height]);
220
+ // The detection loop is started once; it reaches this render's drawing and callbacks here.
221
+ const latestRef = React.useRef({ drawSkeleton, onFrame, onError });
222
+ React.useEffect(() => {
223
+ latestRef.current = { drawSkeleton, onFrame, onError };
224
+ });
180
225
  const handleFrame = React.useCallback((frame) => {
181
- drawSkeleton(frame);
182
- onFrame === null || onFrame === void 0 ? void 0 : onFrame(frame);
183
- }, [drawSkeleton, onFrame]);
226
+ var _a, _b;
227
+ latestRef.current.drawSkeleton(frame);
228
+ (_b = (_a = latestRef.current).onFrame) === null || _b === void 0 ? void 0 : _b.call(_a, frame);
229
+ }, []);
230
+ const handleDetectorError = React.useCallback((error) => {
231
+ var _a, _b;
232
+ (_b = (_a = latestRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error);
233
+ }, []);
184
234
  const handleStart = React.useCallback(async () => {
185
235
  try {
186
236
  if (videoRef.current) {
187
- await detection.start(videoRef.current, handleFrame);
237
+ await detection.start(videoRef.current, handleFrame, handleDetectorError);
188
238
  onStart === null || onStart === void 0 ? void 0 : onStart();
189
239
  }
190
240
  }
191
241
  catch (err) {
192
242
  onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(String(err)));
193
243
  }
194
- }, [detection, handleFrame, onStart, onError]);
244
+ }, [detection, handleFrame, handleDetectorError, onStart, onError]);
195
245
  const handleStop = React.useCallback(() => {
246
+ var _a;
196
247
  detection.stop();
248
+ const canvas = canvasRef.current;
249
+ (_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);
197
250
  onStop === null || onStop === void 0 ? void 0 : onStop();
198
251
  }, [detection, onStop]);
199
252
  const containerStyle = {
@@ -234,9 +287,6 @@ const NicePoseDetector = React.forwardRef(function NicePoseDetector(props, ref)
234
287
  fontWeight: 600,
235
288
  }, children: "Start Camera" }))] }));
236
289
  });
237
- /** @deprecated Use NicePoseDetector */
238
- const PoseDetector = NicePoseDetector;
239
290
 
240
291
  exports.NicePoseDetector = NicePoseDetector;
241
- exports.PoseDetector = PoseDetector;
242
292
  exports.usePoseDetection = usePoseDetection;
package/dist/cjs/index.js CHANGED
@@ -39,9 +39,7 @@ var NiceRenderExport = require('./rendering/NiceRenderExport.js');
39
39
 
40
40
 
41
41
 
42
- exports.ModelEditor = ModelEditor.ModelEditor;
43
42
  exports.NiceModelEditor = ModelEditor.NiceModelEditor;
44
- exports.ModelViewer = ModelViewer.ModelViewer;
45
43
  exports.NiceModelViewer = ModelViewer.NiceModelViewer;
46
44
  exports.DEFAULT_CAD_SETTINGS = cadModeUtils.DEFAULT_CAD_SETTINGS;
47
45
  exports.FLOOR_MATERIALS = cadModeUtils.FLOOR_MATERIALS;
@@ -101,7 +99,6 @@ exports.subclip = modelEditorUtils.subclip;
101
99
  exports.toThreeKeyframeTrack = modelEditorUtils.toThreeKeyframeTrack;
102
100
  exports.EditorShortcutManager = editorShortcuts.EditorShortcutManager;
103
101
  exports.NicePoseDetector = PoseDetector.NicePoseDetector;
104
- exports.PoseDetector = PoseDetector.PoseDetector;
105
102
  exports.usePoseDetection = PoseDetector.usePoseDetection;
106
103
  exports.DanceScoreEngine = DanceScoreEngine.DanceScoreEngine;
107
104
  exports.useDanceScore = DanceScoreEngine.useDanceScore;
@@ -31,8 +31,5 @@ const NiceModelEditor = ({ className, onSaveToLibrary, onAIPoseVideo, }) => {
31
31
  e.target.value = '';
32
32
  } }), jsxRuntime.jsx("input", { ref: api.videoInputRef, type: "file", accept: "video/*", style: { display: 'none' }, onChange: api.handleAIVideo }), jsxRuntime.jsx(ui.NiceAppShell, { className: `${ModelEditor_module.root} ${className !== null && className !== void 0 ? className : ''}`, flush: true, menubar: jsxRuntime.jsx(ModelEditorMenuBar, { api: api, hasOnSaveToLibrary: !!onSaveToLibrary }), sidebar: jsxRuntime.jsxs("div", { style: { display: 'flex', minHeight: 0 }, children: [jsxRuntime.jsx(ModelEditorLeftPanel, { api: api }), jsxRuntime.jsx(ModelEditorToolbar, { api: api })] }), rightSidebar: jsxRuntime.jsx(ModelEditorRightPanel, { api: api }), bottomPanel: jsxRuntime.jsx(ModelEditorTimeline, { api: api }), statusBar: jsxRuntime.jsxs("div", { className: ModelEditor_module.statusBar, children: [jsxRuntime.jsx("span", { children: api.statusText }), jsxRuntime.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: jsxRuntime.jsx(ModelEditorViewport, { api: api }) })] }));
33
33
  };
34
- /** @deprecated Use NiceModelEditor */
35
- const ModelEditor = NiceModelEditor;
36
34
 
37
- exports.ModelEditor = ModelEditor;
38
35
  exports.NiceModelEditor = NiceModelEditor;
@@ -62,8 +62,5 @@ const NiceModelViewer = ({ className, showToolbar = true, showStatusBar = true,
62
62
  a.click();
63
63
  }, title: "Screenshot", "aria-label": "Screenshot", children: jsxRuntime.jsx(ui.NiceIcon, { name: "camera", size: 16 }) }), jsxRuntime.jsx(ui.NiceButton, { className: ModelViewer_module.toolBtn, variant: "ghost", size: "sm", onClick: api.toggleFullscreen, title: "Fullscreen", "aria-label": "Fullscreen", children: jsxRuntime.jsx(ui.NiceIcon, { name: api.isFullscreen ? 'collapse' : 'expand', size: 16 }) }), toolbarExtra, showInfoPanel && (jsxRuntime.jsx("div", { className: ModelViewer_module.toolRight, children: jsxRuntime.jsx(ui.NiceButton, { className: `${ModelViewer_module.toolBtn} ${panelOpen ? ModelViewer_module.active : ''}`, variant: panelOpen ? 'primary' : 'ghost', size: "sm", onClick: () => setPanelOpen(!panelOpen), title: "Info panel", "aria-label": "Info panel", children: jsxRuntime.jsx(ui.NiceIcon, { name: "info", size: 16 }) }) }))] })), jsxRuntime.jsxs("div", { className: ModelViewer_module.viewport, ref: api.mountRef, tabIndex: 0, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [api.loading && (jsxRuntime.jsxs("div", { className: ModelViewer_module.loadingOverlay, children: [jsxRuntime.jsx("span", { className: ModelViewer_module.loadingText, children: "Loading model\u2026" }), jsxRuntime.jsx("div", { className: ModelViewer_module.loadingBar, children: jsxRuntime.jsx("div", { className: ModelViewer_module.loadingBarFill, style: { width: `${(api.loadProgress * 100).toFixed(0)}%` } }) })] })), api.error && (jsxRuntime.jsxs("div", { className: ModelViewer_module.errorOverlay, children: [jsxRuntime.jsx(ui.NiceIcon, { name: "warning", size: 28 }), jsxRuntime.jsx("span", { style: { marginTop: 8 }, children: api.error })] })), dragOver && jsxRuntime.jsx("div", { className: ModelViewer_module.dropZone, children: "Drop 3D model file here" })] }), jsxRuntime.jsxs("div", { className: `${ModelViewer_module.sidePanel} ${!panelOpen ? ModelViewer_module.hidden : ''}`, children: [jsxRuntime.jsxs("div", { className: ModelViewer_module.panelSection, children: [jsxRuntime.jsx("div", { className: ModelViewer_module.panelTitle, children: "Model Info" }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelRow, children: [jsxRuntime.jsx("label", { children: "Triangles" }), jsxRuntime.jsx("span", { children: api.polyCount.toLocaleString() })] }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelRow, children: [jsxRuntime.jsx("label", { children: "Meshes" }), jsxRuntime.jsx("span", { children: api.meshCount })] }), api.dimensions && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: ModelViewer_module.panelRow, children: [jsxRuntime.jsx("label", { children: "Width" }), jsxRuntime.jsx("span", { children: api.dimensions.width })] }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelRow, children: [jsxRuntime.jsx("label", { children: "Height" }), jsxRuntime.jsx("span", { children: api.dimensions.height })] }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelRow, children: [jsxRuntime.jsx("label", { children: "Depth" }), jsxRuntime.jsx("span", { children: api.dimensions.depth })] })] }))] }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelSection, children: [jsxRuntime.jsx("div", { className: ModelViewer_module.panelTitle, children: "Navigation" }), navModes.map((m) => (jsxRuntime.jsx(ui.NiceButton, { className: ModelViewer_module.toolBtn, variant: api.navigationMode === m ? 'primary' : 'ghost', size: "sm", onClick: () => api.setNavigationMode(m), children: jsxRuntime.jsx(NavLabel, { mode: m }) }, m)))] }), jsxRuntime.jsxs("div", { className: ModelViewer_module.panelSection, children: [jsxRuntime.jsx("div", { className: ModelViewer_module.panelTitle, children: "Auto Rotate" }), jsxRuntime.jsx(ui.NiceSlider, { min: 0, max: 10, step: 0.5, value: api.autoRotateSpeed, onChange: (val) => api.setAutoRotateSpeed(val), style: { width: '100%' } })] }), api.animations.length > 0 && (jsxRuntime.jsxs("div", { className: ModelViewer_module.panelSection, children: [jsxRuntime.jsx("div", { className: ModelViewer_module.panelTitle, children: "Animations" }), api.animations.map((name, i) => (jsxRuntime.jsxs(ui.NiceButton, { className: ModelViewer_module.toolBtn, variant: api.activeAnimIdx === i ? 'primary' : 'ghost', size: "sm", onClick: () => api.playAnimation(i), children: [jsxRuntime.jsx(ui.NiceIcon, { name: "play", size: 14 }), " ", name] }, i)))] }))] }), jsxRuntime.jsxs("div", { className: `${ModelViewer_module.animBar} ${api.animations.length === 0 ? ModelViewer_module.hidden : ''}`, children: [jsxRuntime.jsx(ui.NiceButton, { className: ModelViewer_module.toolBtn, variant: "ghost", size: "sm", onClick: api.togglePlay, "aria-label": "Play/Pause", children: jsxRuntime.jsx(ui.NiceIcon, { name: api.isPlaying ? 'pause' : 'play', size: 16 }) }), jsxRuntime.jsx(ui.NiceSelect, { className: ModelViewer_module.animSelect, value: String(api.activeAnimIdx), onChange: (val) => api.playAnimation(parseInt(val)), options: api.animations.map((n, i) => ({ value: String(i), label: n })) }), jsxRuntime.jsx(ui.NiceSlider, { className: ModelViewer_module.animSlider, min: 0, max: api.animDuration || 1, step: 0.01, value: api.animTime, onChange: (val) => api.seekAnim(val) }), jsxRuntime.jsxs("span", { className: ModelViewer_module.toolLabel, children: [api.animTime.toFixed(1), "s / ", api.animDuration.toFixed(1), "s"] })] }), showStatusBar && (jsxRuntime.jsxs("div", { className: ModelViewer_module.statusBar, children: [jsxRuntime.jsx("span", { children: api.statusText }), jsxRuntime.jsxs("div", { className: ModelViewer_module.statusRight, children: [jsxRuntime.jsx("span", { children: jsxRuntime.jsx(NavLabel, { mode: api.navigationMode }) }), api.dimensions && (jsxRuntime.jsxs("span", { children: [api.dimensions.width, "\u00D7", api.dimensions.height, "\u00D7", api.dimensions.depth] }))] })] })), children === null || children === void 0 ? void 0 : children(api)] }));
64
64
  };
65
- /** @deprecated Use NiceModelViewer */
66
- const ModelViewer = NiceModelViewer;
67
65
 
68
- exports.ModelViewer = ModelViewer;
69
66
  exports.NiceModelViewer = NiceModelViewer;
@@ -166,7 +166,7 @@ function useArmatureEditor(skeleton, scene, onBoneSelect) {
166
166
  }
167
167
  }
168
168
  const pose = {
169
- id: crypto.randomUUID(),
169
+ id: ui.createId(),
170
170
  name,
171
171
  timestamp: Date.now(),
172
172
  boneTransforms: transforms,
@@ -243,7 +243,7 @@ const NiceArmatureEditor = ({ skeleton, skinnedMesh: _skinnedMesh, scene, camera
243
243
  padding: '4px 8px',
244
244
  paddingLeft: depth * 16 + 8,
245
245
  background: isSelected ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
246
- color: isSelected ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
246
+ color: isSelected ? 'var(--nice-color-white, #fff)' : 'inherit',
247
247
  cursor: 'pointer',
248
248
  fontSize: 13,
249
249
  borderRadius: 4,
@@ -289,7 +289,7 @@ const NicePhysicsSimulation = ({ scene, selectedObject, onSimulationStep, classN
289
289
  return (jsxRuntime.jsxs("div", { className: `nice-physics-simulation ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxRuntime.jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, children: [jsxRuntime.jsxs("strong", { children: [jsxRuntime.jsx(ui.NiceIcon, { name: "zap", size: 14 }), " Physics"] }), jsxRuntime.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 11 }, children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debugDraw, onChange: (e) => setDebugDraw(e.target.checked) }), "Debug"] })] }), jsxRuntime.jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxRuntime.jsx("button", { type: "button", onClick: isRunning ? pause : play, style: {
290
290
  flex: 1,
291
291
  background: isRunning ? 'var(--nice-danger, #ef4444)' : 'var(--nice-success, #22c55e)',
292
- color: 'var(--nice-text-on-primary, #fff)',
292
+ color: 'var(--nice-color-white, #fff)',
293
293
  border: 'none',
294
294
  borderRadius: 4,
295
295
  padding: '5px 8px',
@@ -364,12 +364,12 @@ const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrai
364
364
  borderRadius: 4,
365
365
  border: '1px solid var(--nice-border, #e2e8f0)',
366
366
  background: brush.mode === m ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
367
- color: brush.mode === m ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
367
+ color: brush.mode === m ? 'var(--nice-color-white, #fff)' : 'inherit',
368
368
  }, children: m }, m)))] }), jsxRuntime.jsxs("label", { children: ["Radius: ", brush.radius.toFixed(1), jsxRuntime.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%' } })] }), jsxRuntime.jsxs("label", { children: ["Strength: ", brush.strength.toFixed(2), jsxRuntime.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%' } })] }), jsxRuntime.jsx("button", { type: "button", onClick: isPainting ? stopPainting : startPainting, style: {
369
369
  background: isPainting
370
370
  ? 'var(--nice-danger, #ef4444)'
371
371
  : 'var(--nice-color-accent, #6366f1)',
372
- color: 'var(--nice-text-on-primary, #fff)',
372
+ color: 'var(--nice-color-white, #fff)',
373
373
  border: 'none',
374
374
  borderRadius: 4,
375
375
  padding: '6px 12px',
@@ -260,7 +260,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
260
260
  flex: 1,
261
261
  padding: '4px 6px',
262
262
  background: brush.mode === mode ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
263
- color: brush.mode === mode ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
263
+ color: brush.mode === mode ? 'var(--nice-color-white, #fff)' : 'inherit',
264
264
  border: '1px solid var(--nice-border, #e2e8f0)',
265
265
  borderRadius: 4,
266
266
  cursor: 'pointer',
@@ -270,7 +270,7 @@ const NiceWeightPainter = ({ skinnedMesh, activeBoneIndex: initialBoneIndex, sce
270
270
  background: isPainting
271
271
  ? 'var(--nice-danger, #ef4444)'
272
272
  : 'var(--nice-color-accent, #6366f1)',
273
- color: 'var(--nice-text-on-primary, #fff)',
273
+ color: 'var(--nice-color-white, #fff)',
274
274
  border: 'none',
275
275
  borderRadius: 4,
276
276
  padding: '6px 12px',
@@ -292,7 +292,7 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
292
292
  borderRadius: 4,
293
293
  cursor: 'pointer',
294
294
  background: mode === 'vr' ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
295
- color: mode === 'vr' ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
295
+ color: mode === 'vr' ? 'var(--nice-color-white, #fff)' : 'inherit',
296
296
  border: '1px solid var(--nice-border, #e2e8f0)',
297
297
  }, children: [jsxRuntime.jsx(ui.NiceIcon, { name: "eye", size: 14 }), " VR"] }), jsxRuntime.jsxs("button", { type: "button", onClick: () => setMode('ar'), style: {
298
298
  flex: 1,
@@ -300,13 +300,13 @@ const NiceXRPreview = ({ renderer, scene, camera, mode: initialMode, onSessionSt
300
300
  borderRadius: 4,
301
301
  cursor: 'pointer',
302
302
  background: mode === 'ar' ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
303
- color: mode === 'ar' ? 'var(--nice-text-on-primary, #fff)' : 'inherit',
303
+ color: mode === 'ar' ? 'var(--nice-color-white, #fff)' : 'inherit',
304
304
  border: '1px solid var(--nice-border, #e2e8f0)',
305
305
  }, children: [jsxRuntime.jsx(ui.NiceIcon, { name: "window", size: 14 }), " AR"] })] }), jsxRuntime.jsx("button", { type: "button", onClick: isSessionActive ? endSession : startSession, style: {
306
306
  background: isSessionActive
307
307
  ? 'var(--nice-danger, #ef4444)'
308
308
  : 'var(--nice-success, #22c55e)',
309
- color: 'var(--nice-text-on-primary, #fff)',
309
+ color: 'var(--nice-color-white, #fff)',
310
310
  border: 'none',
311
311
  borderRadius: 4,
312
312
  padding: '6px 12px',