@viji-dev/sdk 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -63
- package/bin/viji.js +9 -29
- package/dist/assets/artist-dts-BHUsvSI6.js +613 -0
- package/dist/assets/artist-dts-p5-Cyw8vmy_.js +736 -0
- package/dist/assets/core-CiQx3w0t.js +12 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/docs-api-PBLtY4Ni.js +12381 -0
- package/dist/assets/engine-javascript-CXyY7cc8.js +141 -0
- package/dist/assets/essentia-wasm.web-0S-sW98u-CYV1l1zv.js +38 -0
- package/dist/assets/essentia.js-core.es-DnrJE0uR-DOSrF5_G.js +32 -0
- package/dist/assets/glsl-DMyvO4G4.js +1 -0
- package/dist/assets/index-BhFxsauQ.js +215 -0
- package/dist/assets/index-BqhVeA7U.css +1 -0
- package/dist/assets/index-T4TOjvD0.js +1 -0
- package/dist/assets/index-Wz9WqGqz.js +52 -0
- package/dist/assets/index-t24aGwla.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/shader-uniforms-GdaUkQPK.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/viji.worker-CQSJ0SiO-ljtBlcNZ.js +27018 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +31 -35
- package/src/cli/commands/build.js +50 -99
- package/src/cli/commands/create.js +49 -46
- package/src/cli/commands/dev.js +30 -97
- package/src/cli/server/dev-server.js +233 -0
- package/src/cli/server/scene-scanner.js +93 -0
- package/src/cli/server/vite-scene-plugin.d.ts +2 -0
- package/src/cli/server/vite-scene-plugin.js +134 -0
- package/src/cli/utils/cli-utils.js +29 -139
- package/src/cli/utils/scene-compiler.js +10 -17
- package/src/templates/scene-templates.js +85 -0
- package/.gitignore +0 -29
- package/eslint.config.js +0 -37
- package/postcss.config.js +0 -6
- package/scenes/audio-visualizer/main.js +0 -287
- package/scenes/core-demo/main.js +0 -532
- package/scenes/demo-scene/main.js +0 -619
- package/scenes/global.d.ts +0 -15
- package/scenes/particle-system/main.js +0 -349
- package/scenes/tsconfig.json +0 -12
- package/scenes/video-mirror/main.ts +0 -436
- package/src/App.css +0 -42
- package/src/App.tsx +0 -279
- package/src/cli/commands/init.js +0 -262
- package/src/components/SDKPage.tsx +0 -337
- package/src/components/core/CoreContainer.tsx +0 -126
- package/src/components/ui/DeviceSelectionList.tsx +0 -137
- package/src/components/ui/FPSCounter.tsx +0 -78
- package/src/components/ui/FileDropzonePanel.tsx +0 -120
- package/src/components/ui/FileListPanel.tsx +0 -285
- package/src/components/ui/InputExpansionPanel.tsx +0 -31
- package/src/components/ui/MediaPlayerControls.tsx +0 -191
- package/src/components/ui/MenuContainer.tsx +0 -71
- package/src/components/ui/ParametersMenu.tsx +0 -797
- package/src/components/ui/ProjectSwitcherMenu.tsx +0 -192
- package/src/components/ui/QuickInputControls.tsx +0 -542
- package/src/components/ui/SDKMenuSystem.tsx +0 -96
- package/src/components/ui/SettingsMenu.tsx +0 -346
- package/src/components/ui/SimpleInputControls.tsx +0 -137
- package/src/index.css +0 -68
- package/src/main.tsx +0 -10
- package/src/scenes-hmr.ts +0 -158
- package/src/services/project-filesystem.ts +0 -436
- package/src/stores/scene-player/index.ts +0 -3
- package/src/stores/scene-player/input-manager.store.ts +0 -1045
- package/src/stores/scene-player/scene-session.store.ts +0 -659
- package/src/styles/globals.css +0 -111
- package/src/templates/minimal-template.js +0 -11
- package/src/utils/debounce.js +0 -34
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.js +0 -18
- package/tsconfig.app.json +0 -27
- package/tsconfig.json +0 -27
- package/tsconfig.node.json +0 -27
- package/vite.config.ts +0 -54
- /package/{public → dist}/favicon.png +0 -0
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState, useEffect } from 'react';
|
|
2
|
-
import CoreContainer from './core/CoreContainer';
|
|
3
|
-
import FPSCounter from './ui/FPSCounter';
|
|
4
|
-
import SDKMenuSystem from './ui/SDKMenuSystem';
|
|
5
|
-
import QuickInputControls from './ui/QuickInputControls';
|
|
6
|
-
import { useSceneSessionStore } from '../stores/scene-player/scene-session.store';
|
|
7
|
-
|
|
8
|
-
interface Project {
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
path: string;
|
|
12
|
-
mainFile: string;
|
|
13
|
-
lastModified: Date;
|
|
14
|
-
sceneType: 'javascript' | 'typescript' | 'shader';
|
|
15
|
-
isBuilt: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface SDKPageProps {
|
|
19
|
-
sceneCode?: string;
|
|
20
|
-
onSceneError?: (error: string) => void;
|
|
21
|
-
currentProject?: Project;
|
|
22
|
-
onProjectSwitch?: (project: Project) => void;
|
|
23
|
-
// Note: UI CRUD props removed - onProjectCreate, onProjectDelete, onProjectRename no longer used
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const SDKPage: React.FC<SDKPageProps> = ({
|
|
27
|
-
sceneCode,
|
|
28
|
-
onSceneError,
|
|
29
|
-
currentProject,
|
|
30
|
-
onProjectSwitch,
|
|
31
|
-
}) => {
|
|
32
|
-
const coreContainerRef = useRef<HTMLDivElement>(null);
|
|
33
|
-
const lastAppliedCodeRef = useRef<string | null>(null);
|
|
34
|
-
const lastAppliedHashRef = useRef<string | null>(null);
|
|
35
|
-
|
|
36
|
-
// Fast, stable 32-bit FNV-1a hash as hex
|
|
37
|
-
const hashString = (str: string): string => {
|
|
38
|
-
let hash = 2166136261; // FNV offset basis
|
|
39
|
-
for (let i = 0; i < str.length; i++) {
|
|
40
|
-
hash ^= str.charCodeAt(i);
|
|
41
|
-
hash = (hash * 16777619) >>> 0; // FNV prime, convert to unsigned 32-bit
|
|
42
|
-
}
|
|
43
|
-
return hash.toString(16);
|
|
44
|
-
};
|
|
45
|
-
const [showFPS] = useState(false); // TODO: Make this configurable via Settings menu
|
|
46
|
-
const [resolution] = useState(1.0); // TODO: Make this configurable via Settings menu
|
|
47
|
-
const [frameRateMode] = useState<'full' | 'half'>('full'); // TODO: Make this configurable via Settings menu
|
|
48
|
-
|
|
49
|
-
// Debug: Log when sceneCode prop changes (simplified - HMR working)
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
console.log('🔄 [SDKPage] sceneCode prop changed:', {
|
|
52
|
-
len: sceneCode?.length || 0,
|
|
53
|
-
hash: sceneCode ? hashString(sceneCode) : 'none'
|
|
54
|
-
});
|
|
55
|
-
}, [sceneCode, currentProject?.id]);
|
|
56
|
-
|
|
57
|
-
// Use session store for managing the scene session
|
|
58
|
-
const {
|
|
59
|
-
session,
|
|
60
|
-
createSession,
|
|
61
|
-
initializeCore,
|
|
62
|
-
updateSession,
|
|
63
|
-
clearSession,
|
|
64
|
-
destroyCore,
|
|
65
|
-
isInitializingCore,
|
|
66
|
-
coreReady
|
|
67
|
-
} = useSceneSessionStore();
|
|
68
|
-
|
|
69
|
-
// Create session when project changes (with race condition prevention)
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
let isCurrentEffect = true; // Prevent race conditions
|
|
72
|
-
|
|
73
|
-
const switchProject = async () => {
|
|
74
|
-
if (!isCurrentEffect) return;
|
|
75
|
-
|
|
76
|
-
if (!currentProject) {
|
|
77
|
-
if (session) {
|
|
78
|
-
console.log('🧹 Clearing session - no current project');
|
|
79
|
-
await clearSession();
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Only create new session if project changed and effect is still current
|
|
85
|
-
if ((!session || session.sceneId !== currentProject.id) && isCurrentEffect) {
|
|
86
|
-
// Require scene code to be ready for this project before creating session
|
|
87
|
-
if (!sceneCode || sceneCode.trim().length === 0 || !/function\s+render\s*\(/.test(sceneCode)) {
|
|
88
|
-
// Wait until valid scene code is provided via props
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
// Clear old session first to properly destroy previous VijiCore instance
|
|
92
|
-
if (session) {
|
|
93
|
-
console.log('🧹 Clearing previous session for:', session.sceneId);
|
|
94
|
-
await clearSession();
|
|
95
|
-
// Reset hash references when switching projects to prevent false reinit
|
|
96
|
-
lastAppliedCodeRef.current = null;
|
|
97
|
-
lastAppliedHashRef.current = null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Double check effect is still current after async operations
|
|
101
|
-
if (!isCurrentEffect) return;
|
|
102
|
-
|
|
103
|
-
// IMPORTANT: Do NOT reset the input manager here. We want inputs to persist across scenes.
|
|
104
|
-
// Frontend visualizer preserves live input state between scene switches.
|
|
105
|
-
|
|
106
|
-
console.log('🔄 Creating SDK session for new project:', currentProject.name);
|
|
107
|
-
await createSession(currentProject.id, sceneCode, currentProject.name);
|
|
108
|
-
console.log('✅ SDK session created successfully for:', currentProject.name);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
switchProject().catch(console.error);
|
|
113
|
-
|
|
114
|
-
// Cleanup function to prevent race conditions
|
|
115
|
-
return () => {
|
|
116
|
-
isCurrentEffect = false;
|
|
117
|
-
};
|
|
118
|
-
}, [currentProject?.id, session?.sceneId, sceneCode, createSession, clearSession]); // More specific dependencies
|
|
119
|
-
|
|
120
|
-
// Update session when scene code changes (only for the active project)
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
if (!session || !sceneCode) return;
|
|
123
|
-
// Avoid applying code from a different project to current session
|
|
124
|
-
if (!currentProject || session.sceneId !== currentProject.id) return;
|
|
125
|
-
if (session.sceneCode !== sceneCode) {
|
|
126
|
-
const prevHash = hashString(session.sceneCode || '');
|
|
127
|
-
const nextHash = hashString(sceneCode);
|
|
128
|
-
console.log('🔄 Updating scene code for session:', session.sceneId, {
|
|
129
|
-
prevLen: session.sceneCode?.length || 0,
|
|
130
|
-
nextLen: sceneCode.length,
|
|
131
|
-
changed: session.sceneCode !== sceneCode,
|
|
132
|
-
prevHash,
|
|
133
|
-
nextHash,
|
|
134
|
-
equalsFirstChars: session.sceneCode?.slice(0, 64) === sceneCode.slice(0, 64)
|
|
135
|
-
});
|
|
136
|
-
updateSession({ sceneCode });
|
|
137
|
-
} else {
|
|
138
|
-
console.log('⏭️ Scene code unchanged (no update applied):', session.sceneId, {
|
|
139
|
-
len: sceneCode.length
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}, [session?.sceneId, currentProject?.id, sceneCode, updateSession]);
|
|
143
|
-
|
|
144
|
-
// When scene code changes for the active project and a core is running, reinitialize core (like Editor refresh)
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
const reinit = async () => {
|
|
147
|
-
console.log('🔄 [SDKPage] Scene code effect triggered:', {
|
|
148
|
-
hasSession: !!session,
|
|
149
|
-
hasSceneCode: !!sceneCode,
|
|
150
|
-
sceneCodeLen: sceneCode?.length || 0,
|
|
151
|
-
sceneCodeHash: sceneCode ? hashString(sceneCode) : 'none',
|
|
152
|
-
currentProject: currentProject?.id,
|
|
153
|
-
sessionSceneId: session?.sceneId,
|
|
154
|
-
hasCoreInstance: !!session?.coreInstance,
|
|
155
|
-
coreReady,
|
|
156
|
-
isInitializing: isInitializingCore
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (!session || !sceneCode) return;
|
|
160
|
-
if (!currentProject || session.sceneId !== currentProject.id) return;
|
|
161
|
-
if (!session.coreInstance) return;
|
|
162
|
-
// Avoid race with ongoing initialization; only reinit when core is fully ready
|
|
163
|
-
if (isInitializingCore || !coreReady) return;
|
|
164
|
-
// Initial core ready with no applied marker: record and skip
|
|
165
|
-
if (lastAppliedCodeRef.current === null) {
|
|
166
|
-
lastAppliedCodeRef.current = session.sceneCode;
|
|
167
|
-
lastAppliedHashRef.current = hashString(session.sceneCode || '');
|
|
168
|
-
console.log('🔄 [SDKPage] Initial code recorded:', {
|
|
169
|
-
len: session.sceneCode?.length || 0,
|
|
170
|
-
hash: lastAppliedHashRef.current
|
|
171
|
-
});
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Only reinit when new code differs from what core has last applied
|
|
175
|
-
const newHash = hashString(sceneCode);
|
|
176
|
-
if (lastAppliedHashRef.current === newHash) {
|
|
177
|
-
console.log('⏭️ Reinit skipped: code already applied');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Only reinitialize if code contains a render function to prevent worker errors
|
|
182
|
-
if (!/function\s+render\s*\(/.test(sceneCode)) return;
|
|
183
|
-
|
|
184
|
-
console.log('♻️ Reinitializing core due to scene code update for:', session.sceneId, {
|
|
185
|
-
lastAppliedLen: lastAppliedCodeRef.current?.length || 0,
|
|
186
|
-
newLen: sceneCode.length,
|
|
187
|
-
lastAppliedHash: lastAppliedHashRef.current,
|
|
188
|
-
newHash
|
|
189
|
-
});
|
|
190
|
-
try {
|
|
191
|
-
// Mark new code as in-flight to prevent loops
|
|
192
|
-
lastAppliedCodeRef.current = sceneCode;
|
|
193
|
-
lastAppliedHashRef.current = newHash;
|
|
194
|
-
await destroyCore();
|
|
195
|
-
// initCore effect below will pick up and initialize with new code
|
|
196
|
-
} catch (e) {
|
|
197
|
-
console.error('Failed to reinitialize core:', e);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
reinit();
|
|
201
|
-
}, [sceneCode, currentProject?.id, session?.sceneId, session?.coreInstance, session?.sceneCode, isInitializingCore, coreReady, destroyCore]);
|
|
202
|
-
|
|
203
|
-
// Update core config when settings change
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
if (session) {
|
|
206
|
-
updateSession({
|
|
207
|
-
coreConfig: {
|
|
208
|
-
resolution,
|
|
209
|
-
frameRateMode,
|
|
210
|
-
allowUserInteraction: true
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}, [session?.sceneId, resolution, frameRateMode, updateSession]); // Only run when session ID or settings change
|
|
215
|
-
|
|
216
|
-
// Initialize core when session is ready (with race condition prevention)
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
let isCurrentEffect = true; // Prevent race conditions
|
|
219
|
-
|
|
220
|
-
const initCore = async () => {
|
|
221
|
-
if (!isCurrentEffect) return;
|
|
222
|
-
|
|
223
|
-
// Only initialize if this effect is still current and conditions are met
|
|
224
|
-
if (!session || !session.sceneCode || session.coreInstance || !coreContainerRef.current) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
// Ensure scene code contains a render() function to avoid worker errors
|
|
228
|
-
if (!/function\s+render\s*\(/.test(session.sceneCode)) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const container = coreContainerRef.current.querySelector('#viji-core-host') as HTMLElement;
|
|
233
|
-
if (!container) {
|
|
234
|
-
console.warn('🚫 Skipping Core initialization: container not ready');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
console.log('🚀 Initializing core for session:', session.sceneId);
|
|
240
|
-
await initializeCore(container);
|
|
241
|
-
|
|
242
|
-
if (isCurrentEffect) {
|
|
243
|
-
console.log('✅ Core initialized successfully for:', session.sceneId);
|
|
244
|
-
// After successful init, record the code currently running in core
|
|
245
|
-
try {
|
|
246
|
-
lastAppliedCodeRef.current = session.sceneCode;
|
|
247
|
-
lastAppliedHashRef.current = hashString(session.sceneCode || '');
|
|
248
|
-
} catch {}
|
|
249
|
-
}
|
|
250
|
-
} catch (error) {
|
|
251
|
-
if (isCurrentEffect) {
|
|
252
|
-
console.error('❌ Failed to initialize core:', error);
|
|
253
|
-
onSceneError?.(error instanceof Error ? error.message : 'Failed to initialize scene');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
initCore();
|
|
259
|
-
|
|
260
|
-
// Cleanup function to prevent race conditions
|
|
261
|
-
return () => {
|
|
262
|
-
isCurrentEffect = false;
|
|
263
|
-
};
|
|
264
|
-
}, [session?.sceneId, session?.sceneCode, session?.coreInstance, initializeCore, onSceneError]); // Re-run when session changes
|
|
265
|
-
|
|
266
|
-
// Settings and FPS toggle handlers would be implemented when needed
|
|
267
|
-
// const handleSettingsChange = (settings: any) => {
|
|
268
|
-
// if (settings.resolution !== undefined) setResolution(settings.resolution);
|
|
269
|
-
// if (settings.frameRateMode !== undefined) setFrameRateMode(settings.frameRateMode);
|
|
270
|
-
// };
|
|
271
|
-
// const handleFPSToggle = (show: boolean) => setShowFPS(show);
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<div className="fixed inset-0 bg-black overflow-hidden">
|
|
275
|
-
{/* Core container - Full screen since no editor sidebar */}
|
|
276
|
-
<div className="w-full h-full relative">
|
|
277
|
-
{/* Core host */}
|
|
278
|
-
<div
|
|
279
|
-
ref={coreContainerRef}
|
|
280
|
-
className="w-full h-full"
|
|
281
|
-
>
|
|
282
|
-
{session ? (
|
|
283
|
-
<CoreContainer
|
|
284
|
-
session={session}
|
|
285
|
-
className="w-full h-full"
|
|
286
|
-
onError={onSceneError}
|
|
287
|
-
/>
|
|
288
|
-
) : (
|
|
289
|
-
<div className="w-full h-full flex items-center justify-center text-white/60">
|
|
290
|
-
<div className="text-center">
|
|
291
|
-
<div className="text-white">No scene loaded</div>
|
|
292
|
-
<div className="text-sm">Switch to a project to load a scene</div>
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
)}
|
|
296
|
-
</div>
|
|
297
|
-
|
|
298
|
-
{/* Overlay: top header with logo */}
|
|
299
|
-
<div className="absolute top-0 left-0 right-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
|
300
|
-
<div className="flex items-center justify-center pointer-events-auto">
|
|
301
|
-
<div className="bg-black/20 backdrop-blur-sm rounded-lg px-4 py-2 border border-white/10">
|
|
302
|
-
<div className="flex items-center space-x-2">
|
|
303
|
-
<div className="w-8 h-8 bg-gradient-to-br from-primary-400 to-primary-600 rounded-lg flex items-center justify-center">
|
|
304
|
-
<span className="text-white font-bold text-sm">V</span>
|
|
305
|
-
</div>
|
|
306
|
-
<span className="text-white font-semibold tracking-wide">Viji SDK</span>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
{/* Menu system - Projects, Parameters, Settings (always visible like in play mode) */}
|
|
313
|
-
<SDKMenuSystem
|
|
314
|
-
currentProject={currentProject}
|
|
315
|
-
onProjectSwitch={onProjectSwitch}
|
|
316
|
-
/>
|
|
317
|
-
|
|
318
|
-
{/* Bottom-right input controls - place directly to avoid full-screen overlay capturing clicks */}
|
|
319
|
-
<QuickInputControls
|
|
320
|
-
className="bottom-4 right-4 z-60"
|
|
321
|
-
onSetControlsPinned={(pinned) => {
|
|
322
|
-
console.log('🔧 Controls pinned:', pinned);
|
|
323
|
-
}}
|
|
324
|
-
/>
|
|
325
|
-
|
|
326
|
-
{/* FPS Counter - bottom center */}
|
|
327
|
-
<FPSCounter
|
|
328
|
-
className="bottom-6 absolute left-1/2 transform -translate-x-1/2"
|
|
329
|
-
show={showFPS}
|
|
330
|
-
coreInstance={session?.coreInstance}
|
|
331
|
-
/>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
);
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
export default SDKPage;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { forwardRef, useEffect, useRef } from 'react';
|
|
2
|
-
import type { ScenePlayerSession } from '../../stores/scene-player/scene-session.store';
|
|
3
|
-
|
|
4
|
-
interface CoreContainerProps {
|
|
5
|
-
className?: string;
|
|
6
|
-
session: ScenePlayerSession;
|
|
7
|
-
onError?: (error: string) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const CoreContainer = forwardRef<HTMLDivElement, CoreContainerProps>(
|
|
11
|
-
({ className = '', session, onError }, ref) => {
|
|
12
|
-
const mountedRef = useRef(false);
|
|
13
|
-
const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
14
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
15
|
-
|
|
16
|
-
const scheduleResolutionUpdate = () => {
|
|
17
|
-
if (!session.coreInstance || !mountedRef.current) return;
|
|
18
|
-
// Avoid propagating zero-size updates that can destabilize the core
|
|
19
|
-
const el = containerRef.current;
|
|
20
|
-
if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
|
|
21
|
-
if (resizeTimeoutRef.current) {
|
|
22
|
-
clearTimeout(resizeTimeoutRef.current);
|
|
23
|
-
}
|
|
24
|
-
resizeTimeoutRef.current = setTimeout(async () => {
|
|
25
|
-
const elt = containerRef.current;
|
|
26
|
-
if (
|
|
27
|
-
session.coreInstance &&
|
|
28
|
-
mountedRef.current &&
|
|
29
|
-
elt &&
|
|
30
|
-
elt.clientWidth > 0 &&
|
|
31
|
-
elt.clientHeight > 0
|
|
32
|
-
) {
|
|
33
|
-
try {
|
|
34
|
-
await session.coreInstance.updateResolution();
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.warn('⚠️ Error updating Core resolution:', error);
|
|
37
|
-
onError?.('Failed to update scene resolution');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}, 50);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Handle window resize events with debouncing
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
const handleResize = () => scheduleResolutionUpdate();
|
|
46
|
-
window.addEventListener('resize', handleResize);
|
|
47
|
-
return () => {
|
|
48
|
-
window.removeEventListener('resize', handleResize);
|
|
49
|
-
if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current);
|
|
50
|
-
};
|
|
51
|
-
}, [session.coreInstance]);
|
|
52
|
-
|
|
53
|
-
// Observe container size changes (e.g., padding changes when editor sidebar resizes)
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (!containerRef.current) return;
|
|
56
|
-
if (!(window as any).ResizeObserver) return;
|
|
57
|
-
const ro = new (window as any).ResizeObserver(() => {
|
|
58
|
-
scheduleResolutionUpdate();
|
|
59
|
-
});
|
|
60
|
-
ro.observe(containerRef.current);
|
|
61
|
-
return () => {
|
|
62
|
-
try { ro.disconnect(); } catch {}
|
|
63
|
-
};
|
|
64
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
-
}, [containerRef.current, session.coreInstance]);
|
|
66
|
-
|
|
67
|
-
// Handle core errors
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (session.coreInstance && mountedRef.current) {
|
|
70
|
-
// Listen for core errors if the API supports it
|
|
71
|
-
// This would depend on the actual @viji-dev/core API
|
|
72
|
-
try {
|
|
73
|
-
// session.coreInstance.onError?.(onError);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.warn('Core error handling not available:', error);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}, [session.coreInstance, onError]);
|
|
79
|
-
|
|
80
|
-
// Track mounting state
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
mountedRef.current = true;
|
|
83
|
-
return () => {
|
|
84
|
-
mountedRef.current = false;
|
|
85
|
-
};
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div
|
|
90
|
-
ref={containerRef}
|
|
91
|
-
className={`relative bg-black ${className}`}
|
|
92
|
-
style={{
|
|
93
|
-
width: '100%',
|
|
94
|
-
height: '100%',
|
|
95
|
-
overflow: 'hidden',
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
{/* VijiCore Container - React hands control to VijiCore */}
|
|
99
|
-
<div
|
|
100
|
-
id="viji-core-host"
|
|
101
|
-
ref={ref}
|
|
102
|
-
className="absolute inset-0 bg-black"
|
|
103
|
-
style={{
|
|
104
|
-
width: '100%',
|
|
105
|
-
height: '100%',
|
|
106
|
-
}}
|
|
107
|
-
// This div is for VijiCore only - no React children!
|
|
108
|
-
/>
|
|
109
|
-
|
|
110
|
-
{/* Loading Overlay - Managed by React, separate from VijiCore */}
|
|
111
|
-
{!session.coreInstance && mountedRef.current && session.sceneCode && (
|
|
112
|
-
<div className="absolute inset-0 flex items-center justify-center text-white/50 pointer-events-none z-10">
|
|
113
|
-
<div className="text-center">
|
|
114
|
-
<div className="w-8 h-8 border-2 border-white/20 border-t-white/60 rounded-full animate-spin mx-auto mb-4"></div>
|
|
115
|
-
<p>Preparing scene...</p>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
CoreContainer.displayName = 'CoreContainer';
|
|
125
|
-
|
|
126
|
-
export default CoreContainer;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { Button } from '@heroui/react';
|
|
3
|
-
import { CheckIcon } from '@heroicons/react/24/outline';
|
|
4
|
-
import { useInputManagerStore } from '../../stores/scene-player/input-manager.store';
|
|
5
|
-
|
|
6
|
-
interface DeviceSelectionListProps {
|
|
7
|
-
deviceType: 'audio' | 'video';
|
|
8
|
-
onDeviceSelect?: (deviceId: string, deviceLabel: string) => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const DeviceSelectionList: React.FC<DeviceSelectionListProps> = ({
|
|
12
|
-
deviceType,
|
|
13
|
-
onDeviceSelect
|
|
14
|
-
}) => {
|
|
15
|
-
const {
|
|
16
|
-
availableDevices,
|
|
17
|
-
inputConfiguration,
|
|
18
|
-
refreshAvailableDevices,
|
|
19
|
-
selectAudioDevice,
|
|
20
|
-
selectVideoDevice,
|
|
21
|
-
requestPermissions,
|
|
22
|
-
syncDevicesFromActiveStreams
|
|
23
|
-
} = useInputManagerStore();
|
|
24
|
-
|
|
25
|
-
const currentDeviceId = deviceType === 'audio'
|
|
26
|
-
? inputConfiguration.audio.deviceId
|
|
27
|
-
: inputConfiguration.video.deviceId;
|
|
28
|
-
|
|
29
|
-
const devices = deviceType === 'audio'
|
|
30
|
-
? availableDevices.audioDevices
|
|
31
|
-
: availableDevices.videoDevices;
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
let cancelled = false;
|
|
35
|
-
const loadDevices = async () => {
|
|
36
|
-
// Request permissions first to ensure we can see device labels
|
|
37
|
-
const permissionGranted = await requestPermissions(
|
|
38
|
-
deviceType === 'audio',
|
|
39
|
-
deviceType === 'video'
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
if (!permissionGranted) return;
|
|
43
|
-
if (cancelled) return;
|
|
44
|
-
await refreshAvailableDevices();
|
|
45
|
-
if (cancelled) return;
|
|
46
|
-
// After enumeration, sync active stream device IDs to ensure first-open highlighting
|
|
47
|
-
syncDevicesFromActiveStreams();
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Slight delay to avoid clashing with popover open focus/aria changes
|
|
51
|
-
const t = setTimeout(() => { loadDevices().catch(console.error); }, 50);
|
|
52
|
-
return () => { cancelled = true; clearTimeout(t); };
|
|
53
|
-
}, [deviceType, requestPermissions, refreshAvailableDevices, syncDevicesFromActiveStreams]);
|
|
54
|
-
|
|
55
|
-
// Proactive sync when devices or streams change to keep highlighting correct without reopening
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
syncDevicesFromActiveStreams();
|
|
58
|
-
}, [
|
|
59
|
-
syncDevicesFromActiveStreams,
|
|
60
|
-
availableDevices.audioDevices,
|
|
61
|
-
availableDevices.videoDevices,
|
|
62
|
-
inputConfiguration.audio.stream,
|
|
63
|
-
inputConfiguration.video.stream
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
// Debug: Track highlight changes
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
console.log(`🎯 [HIGHLIGHT] ${deviceType} currentDeviceId changed`, {
|
|
69
|
-
deviceType,
|
|
70
|
-
currentDeviceId,
|
|
71
|
-
devices: devices.map(d => ({ id: d.deviceId, label: d.label })),
|
|
72
|
-
isMatchAny: devices.some(d => d.deviceId === currentDeviceId)
|
|
73
|
-
});
|
|
74
|
-
}, [currentDeviceId, deviceType]);
|
|
75
|
-
|
|
76
|
-
const handleDeviceSelect = async (deviceId: string, deviceLabel: string) => {
|
|
77
|
-
try {
|
|
78
|
-
if (deviceType === 'audio') {
|
|
79
|
-
await selectAudioDevice(deviceId, deviceLabel);
|
|
80
|
-
} else {
|
|
81
|
-
await selectVideoDevice(deviceId, deviceLabel);
|
|
82
|
-
}
|
|
83
|
-
onDeviceSelect?.(deviceId, deviceLabel);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error(`❌ [DeviceSelectionList] Failed to select ${deviceType} device:`, error);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (devices.length === 0) {
|
|
90
|
-
return (
|
|
91
|
-
<div className="text-center py-8 text-gray-400">
|
|
92
|
-
<p>No {deviceType} devices found</p>
|
|
93
|
-
<Button
|
|
94
|
-
size="sm"
|
|
95
|
-
variant="flat"
|
|
96
|
-
className="mt-2 bg-white/10 text-white hover:bg-white/20"
|
|
97
|
-
onPress={() => refreshAvailableDevices()}
|
|
98
|
-
>
|
|
99
|
-
Refresh Devices
|
|
100
|
-
</Button>
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div className="space-y-2">
|
|
107
|
-
{devices.map((device) => {
|
|
108
|
-
const isSelected = device.deviceId === currentDeviceId;
|
|
109
|
-
const deviceName = device.label || `${deviceType} Device`;
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<Button
|
|
113
|
-
key={device.deviceId}
|
|
114
|
-
variant="flat"
|
|
115
|
-
className={`w-full justify-start px-3 py-2 h-auto min-h-0 pointer-events-auto ${
|
|
116
|
-
isSelected
|
|
117
|
-
? 'bg-white/20 text-white'
|
|
118
|
-
: 'bg-transparent text-gray-300 hover:bg-white/10 hover:text-white'
|
|
119
|
-
}`}
|
|
120
|
-
onPress={() => handleDeviceSelect(device.deviceId, deviceName)}
|
|
121
|
-
>
|
|
122
|
-
<div className="flex items-center w-full">
|
|
123
|
-
<div className="flex-shrink-0 w-5 h-5 mr-3 flex items-center justify-center">
|
|
124
|
-
{isSelected && (
|
|
125
|
-
<CheckIcon className="w-4 h-4 text-green-400" />
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
<span className="text-left truncate">{deviceName}</span>
|
|
129
|
-
</div>
|
|
130
|
-
</Button>
|
|
131
|
-
);
|
|
132
|
-
})}
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export default DeviceSelectionList;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
interface FPSCounterProps {
|
|
4
|
-
className?: string;
|
|
5
|
-
show?: boolean;
|
|
6
|
-
coreInstance?: any;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const FPSCounter: React.FC<FPSCounterProps> = ({
|
|
10
|
-
className = '',
|
|
11
|
-
show = false,
|
|
12
|
-
coreInstance
|
|
13
|
-
}) => {
|
|
14
|
-
const [effectiveRefreshRate, setEffectiveRefreshRate] = useState<number | null>(null);
|
|
15
|
-
const rafRef = useRef<number | null>(null);
|
|
16
|
-
const lastUpdateRef = useRef<number>(0);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!show) {
|
|
20
|
-
setEffectiveRefreshRate(null);
|
|
21
|
-
}
|
|
22
|
-
}, [show]);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
const update = (now: number) => {
|
|
26
|
-
if (!show) return;
|
|
27
|
-
// Throttle to ~2 Hz to keep overhead negligible
|
|
28
|
-
if (now - lastUpdateRef.current < 500) {
|
|
29
|
-
rafRef.current = requestAnimationFrame(update);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
lastUpdateRef.current = now;
|
|
33
|
-
try {
|
|
34
|
-
const stats = coreInstance?.getStats?.();
|
|
35
|
-
const value = stats?.frameRate?.effectiveRefreshRate;
|
|
36
|
-
if (typeof value === 'number' && !Number.isNaN(value)) {
|
|
37
|
-
setEffectiveRefreshRate(value);
|
|
38
|
-
} else {
|
|
39
|
-
setEffectiveRefreshRate(null);
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
setEffectiveRefreshRate(null);
|
|
43
|
-
}
|
|
44
|
-
rafRef.current = requestAnimationFrame(update);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (show) {
|
|
48
|
-
rafRef.current = requestAnimationFrame(update);
|
|
49
|
-
return () => {
|
|
50
|
-
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
51
|
-
rafRef.current = null;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}, [show, coreInstance]);
|
|
56
|
-
|
|
57
|
-
if (!show) return null;
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div
|
|
61
|
-
className={
|
|
62
|
-
`pointer-events-none absolute left-1/2 transform -translate-x-1/2 z-60 ` +
|
|
63
|
-
`bg-black/60 backdrop-blur-sm text-white px-3 py-1 rounded-full border border-white/20 ` +
|
|
64
|
-
`text-xs font-medium ${className}`
|
|
65
|
-
}
|
|
66
|
-
aria-label="Frames per second"
|
|
67
|
-
role="status"
|
|
68
|
-
>
|
|
69
|
-
{effectiveRefreshRate !== null ? `${Math.round(effectiveRefreshRate)} FPS` : '— FPS'}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export default FPSCounter;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|