@viji-dev/sdk 1.0.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 (55) hide show
  1. package/.gitignore +29 -0
  2. package/LICENSE +13 -0
  3. package/README.md +103 -0
  4. package/bin/viji.js +75 -0
  5. package/eslint.config.js +37 -0
  6. package/index.html +20 -0
  7. package/package.json +82 -0
  8. package/postcss.config.js +6 -0
  9. package/public/favicon.png +0 -0
  10. package/scenes/audio-visualizer/main.js +287 -0
  11. package/scenes/core-demo/main.js +532 -0
  12. package/scenes/demo-scene/main.js +619 -0
  13. package/scenes/global.d.ts +15 -0
  14. package/scenes/particle-system/main.js +349 -0
  15. package/scenes/tsconfig.json +12 -0
  16. package/scenes/video-mirror/main.ts +436 -0
  17. package/src/App.css +42 -0
  18. package/src/App.tsx +279 -0
  19. package/src/cli/commands/build.js +147 -0
  20. package/src/cli/commands/create.js +71 -0
  21. package/src/cli/commands/dev.js +108 -0
  22. package/src/cli/commands/init.js +262 -0
  23. package/src/cli/utils/cli-utils.js +208 -0
  24. package/src/cli/utils/scene-compiler.js +432 -0
  25. package/src/components/SDKPage.tsx +337 -0
  26. package/src/components/core/CoreContainer.tsx +126 -0
  27. package/src/components/ui/DeviceSelectionList.tsx +137 -0
  28. package/src/components/ui/FPSCounter.tsx +78 -0
  29. package/src/components/ui/FileDropzonePanel.tsx +120 -0
  30. package/src/components/ui/FileListPanel.tsx +285 -0
  31. package/src/components/ui/InputExpansionPanel.tsx +31 -0
  32. package/src/components/ui/MediaPlayerControls.tsx +191 -0
  33. package/src/components/ui/MenuContainer.tsx +71 -0
  34. package/src/components/ui/ParametersMenu.tsx +797 -0
  35. package/src/components/ui/ProjectSwitcherMenu.tsx +192 -0
  36. package/src/components/ui/QuickInputControls.tsx +542 -0
  37. package/src/components/ui/SDKMenuSystem.tsx +96 -0
  38. package/src/components/ui/SettingsMenu.tsx +346 -0
  39. package/src/components/ui/SimpleInputControls.tsx +137 -0
  40. package/src/index.css +68 -0
  41. package/src/main.tsx +10 -0
  42. package/src/scenes-hmr.ts +158 -0
  43. package/src/services/project-filesystem.ts +436 -0
  44. package/src/stores/scene-player/index.ts +3 -0
  45. package/src/stores/scene-player/input-manager.store.ts +1045 -0
  46. package/src/stores/scene-player/scene-session.store.ts +659 -0
  47. package/src/styles/globals.css +111 -0
  48. package/src/templates/minimal-template.js +11 -0
  49. package/src/utils/debounce.js +34 -0
  50. package/src/vite-env.d.ts +1 -0
  51. package/tailwind.config.js +18 -0
  52. package/tsconfig.app.json +27 -0
  53. package/tsconfig.json +27 -0
  54. package/tsconfig.node.json +27 -0
  55. package/vite.config.ts +54 -0
@@ -0,0 +1,346 @@
1
+ import React from 'react';
2
+ import {
3
+ Card,
4
+ CardBody,
5
+ Switch,
6
+ Select,
7
+ SelectItem,
8
+ Button,
9
+ Divider,
10
+ Chip
11
+ } from '@heroui/react';
12
+ import {
13
+ CogIcon,
14
+ ComputerDesktopIcon,
15
+ EyeIcon,
16
+ ClockIcon
17
+ } from '@heroicons/react/24/outline';
18
+ import { useSceneSessionStore } from '../../stores/scene-player/scene-session.store';
19
+
20
+ interface SettingsMenuProps {
21
+ className?: string;
22
+ }
23
+
24
+ const SettingsMenu: React.FC<SettingsMenuProps> = ({ className = '' }) => {
25
+ const showFPS = useSceneSessionStore((s) => s.uiPreferences?.showFPSCounter ?? false);
26
+
27
+ // Store selectors
28
+ const session = useSceneSessionStore((state) => state.session);
29
+ const updateCoreConfig = useSceneSessionStore((state) => state.updateCoreConfig);
30
+ const setShowFPSCounter = useSceneSessionStore((state) => state.setShowFPSCounter);
31
+
32
+ // Get current settings from core config
33
+ const currentResolution = (session?.coreConfig as any)?.resolution ?? 1.0;
34
+ const currentFrameRateMode = session?.coreConfig?.frameRateMode || 'full';
35
+
36
+ const handleResolutionChange = async (resolution: string) => {
37
+ const resolutionValue = parseFloat(resolution);
38
+ try {
39
+ await updateCoreConfig({ resolution: resolutionValue });
40
+ } catch (error) {
41
+ console.error('Failed to update resolution:', error);
42
+ }
43
+ };
44
+
45
+ const handleFrameRateChange = async (frameRateMode: string) => {
46
+ try {
47
+ await updateCoreConfig({ frameRateMode: frameRateMode as 'full' | 'half' });
48
+ } catch (error) {
49
+ console.error('Failed to update frame rate:', error);
50
+ }
51
+ };
52
+
53
+ const handleShowFPSChange = (show: boolean) => {
54
+ setShowFPSCounter(show);
55
+ };
56
+
57
+ const getResolutionLabel = (resolution: number) => {
58
+ switch (resolution) {
59
+ case 1.0:
60
+ return 'Full (1.0x)';
61
+ case 0.5:
62
+ return 'Half (0.5x)';
63
+ case 0.75:
64
+ return 'High (0.75x)';
65
+ case 0.25:
66
+ return 'Low (0.25x)';
67
+ default:
68
+ return `Custom (${resolution}x)`;
69
+ }
70
+ };
71
+
72
+ const getFrameRateLabel = (mode: string) => {
73
+ switch (mode) {
74
+ case 'full':
75
+ return 'Full (60 FPS)';
76
+ case 'half':
77
+ return 'Half (30 FPS)';
78
+ default:
79
+ return mode;
80
+ }
81
+ };
82
+
83
+ const resolutionOptions = [
84
+ { value: '1.0', label: 'Full Resolution', description: '100% of canvas size (recommended)' },
85
+ { value: '0.75', label: 'High Resolution', description: '75% of canvas size' },
86
+ { value: '0.5', label: 'Half Resolution', description: '50% of canvas size' },
87
+ { value: '0.25', label: 'Low Resolution', description: '25% of canvas size' },
88
+ ];
89
+
90
+ const frameRateOptions = [
91
+ { value: 'full', label: 'Full Frame Rate', description: '60 FPS (smooth)' },
92
+ { value: 'half', label: 'Half Frame Rate', description: '30 FPS (battery saving)' },
93
+ ];
94
+
95
+ // Ensure the selected keys match available options
96
+ const selectedResolutionKey = resolutionOptions.find(opt => parseFloat(opt.value) === currentResolution)?.value || '1.0';
97
+ const selectedFrameRateKey = frameRateOptions.find(opt => opt.value === currentFrameRateMode)?.value || 'full';
98
+
99
+ if (!session) {
100
+ return (
101
+ <div className="p-4 text-center text-white/60">
102
+ <p>No active scene</p>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <div className={`h-full p-4 overflow-y-auto ${className}`}>
109
+ {/* Render Settings Section */}
110
+ <div className="space-y-6">
111
+ <Card className="bg-white/5">
112
+ <CardBody className="p-4">
113
+ <div className="flex items-center space-x-2 mb-4">
114
+ <ComputerDesktopIcon className="w-5 h-5 text-primary-400" />
115
+ <h3 className="text-white font-medium">Render Settings</h3>
116
+ </div>
117
+
118
+ <div className="space-y-4">
119
+ {/* Resolution Setting */}
120
+ <div>
121
+ <div className="flex items-center justify-between mb-2">
122
+ <label className="text-white text-sm font-medium">
123
+ Resolution
124
+ </label>
125
+ <Chip
126
+ size="sm"
127
+ variant="flat"
128
+ color="primary"
129
+ className="text-xs"
130
+ >
131
+ {getResolutionLabel(currentResolution)}
132
+ </Chip>
133
+ </div>
134
+
135
+ <Select
136
+ selectedKeys={[selectedResolutionKey]}
137
+ onSelectionChange={(keys) => {
138
+ const selectedValue = Array.from(keys)[0];
139
+ if (selectedValue) {
140
+ handleResolutionChange(selectedValue as string);
141
+ }
142
+ }}
143
+ variant="bordered"
144
+ size="sm"
145
+ className="w-full visualizer-select [&_[data-slot=value]]:!text-white [&_[data-slot=value]]:!opacity-100"
146
+ placeholder="Select resolution"
147
+ aria-label="Resolution setting"
148
+ classNames={{
149
+ trigger: "bg-black/20 border-white/20 text-white data-[hover=true]:bg-black/30",
150
+ label: "text-white/80",
151
+ value: "!text-white !opacity-100",
152
+ innerWrapper: "text-white",
153
+ selectorIcon: "text-white/60",
154
+ listbox: "bg-black/90",
155
+ popoverContent: "bg-black/90 border border-white/20"
156
+ }}
157
+ style={{
158
+ // @ts-ignore - Force override HeroUI CSS variables
159
+ "--nextui-content1": "white",
160
+ "--nextui-content1-foreground": "white",
161
+ "--nextui-foreground": "white"
162
+ } as any}
163
+ >
164
+ {resolutionOptions.map((option) => (
165
+ <SelectItem
166
+ key={option.value}
167
+ textValue={option.label}
168
+ className="text-white data-[hover=true]:bg-white/10"
169
+
170
+ >
171
+ <div className="flex flex-col">
172
+ <span className="text-sm font-medium text-white">{option.label}</span>
173
+ <span className="text-xs text-white/60">{option.description}</span>
174
+ </div>
175
+ </SelectItem>
176
+ ))}
177
+ </Select>
178
+
179
+ <p className="text-xs text-white/60 mt-1">
180
+ Lower resolution multiplier improves performance but reduces visual quality
181
+ </p>
182
+ </div>
183
+
184
+ {/* Frame Rate Setting */}
185
+ <div>
186
+ <div className="flex items-center justify-between mb-2">
187
+ <label className="text-white text-sm font-medium">
188
+ Frame Rate
189
+ </label>
190
+ <Chip
191
+ size="sm"
192
+ variant="flat"
193
+ color="secondary"
194
+ className="text-xs"
195
+ >
196
+ {getFrameRateLabel(currentFrameRateMode)}
197
+ </Chip>
198
+ </div>
199
+
200
+ <Select
201
+ selectedKeys={[selectedFrameRateKey]}
202
+ onSelectionChange={(keys) => {
203
+ const selectedValue = Array.from(keys)[0];
204
+ if (selectedValue) {
205
+ handleFrameRateChange(selectedValue as string);
206
+ }
207
+ }}
208
+ variant="bordered"
209
+ size="sm"
210
+ className="w-full visualizer-select [&_[data-slot=value]]:!text-white [&_[data-slot=value]]:!opacity-100"
211
+ placeholder="Select frame rate"
212
+ aria-label="Frame rate setting"
213
+ classNames={{
214
+ trigger: "bg-black/20 border-white/20 text-white data-[hover=true]:bg-black/30",
215
+ label: "text-white/80",
216
+ value: "!text-white !opacity-100",
217
+ innerWrapper: "text-white",
218
+ selectorIcon: "text-white/60",
219
+ listbox: "bg-black/90",
220
+ popoverContent: "bg-black/90 border border-white/20"
221
+ }}
222
+ style={{
223
+ // @ts-ignore - Force override HeroUI CSS variables
224
+ "--nextui-content1": "white",
225
+ "--nextui-content1-foreground": "white",
226
+ "--nextui-foreground": "white"
227
+ } as any}
228
+ >
229
+ {frameRateOptions.map((option) => (
230
+ <SelectItem
231
+ key={option.value}
232
+ textValue={option.label}
233
+ className="text-white data-[hover=true]:bg-white/10"
234
+
235
+ >
236
+ <div className="flex flex-col">
237
+ <span className="text-sm font-medium text-white">{option.label}</span>
238
+ <span className="text-xs text-white/60">{option.description}</span>
239
+ </div>
240
+ </SelectItem>
241
+ ))}
242
+ </Select>
243
+
244
+ <p className="text-xs text-white/60 mt-1">
245
+ Half frame rate reduces CPU usage and extends battery life
246
+ </p>
247
+ </div>
248
+ </div>
249
+ </CardBody>
250
+ </Card>
251
+
252
+ {/* Interface Settings Section */}
253
+ <Card className="bg-white/5">
254
+ <CardBody className="p-4">
255
+ <div className="flex items-center space-x-2 mb-4">
256
+ <EyeIcon className="w-5 h-5 text-secondary-400" />
257
+ <h3 className="text-white font-medium">Interface Settings</h3>
258
+ </div>
259
+
260
+ <div className="space-y-4">
261
+ {/* Show FPS Setting */}
262
+ <div className="flex items-center justify-between">
263
+ <div className="flex-1">
264
+ <div className="flex items-center space-x-2">
265
+ <ClockIcon className="w-4 h-4 text-white/60" />
266
+ <label className="text-white text-sm font-medium">
267
+ Show FPS Counter
268
+ </label>
269
+ </div>
270
+ <p className="text-xs text-white/60 mt-1">
271
+ Display real-time frames per second in the bottom center
272
+ </p>
273
+ </div>
274
+ <Switch
275
+ isSelected={showFPS}
276
+ onValueChange={handleShowFPSChange}
277
+ color="secondary"
278
+ size="sm"
279
+ />
280
+ </div>
281
+ </div>
282
+ </CardBody>
283
+ </Card>
284
+
285
+ {/* Performance Info Section */}
286
+ <Card className="bg-white/5">
287
+ <CardBody className="p-4">
288
+ <div className="flex items-center space-x-2 mb-4">
289
+ <CogIcon className="w-5 h-5 text-success-400" />
290
+ <h3 className="text-white font-medium">Performance Info</h3>
291
+ </div>
292
+
293
+ <div className="space-y-3">
294
+ <div className="flex justify-between text-sm">
295
+ <span className="text-white/60">Current Resolution:</span>
296
+ <span className="text-white">{getResolutionLabel(currentResolution)}</span>
297
+ </div>
298
+
299
+ <div className="flex justify-between text-sm">
300
+ <span className="text-white/60">Frame Rate Mode:</span>
301
+ <span className="text-white">{getFrameRateLabel(currentFrameRateMode)}</span>
302
+ </div>
303
+
304
+ <div className="flex justify-between text-sm">
305
+ <span className="text-white/60">Core Status:</span>
306
+ <span className="text-white">
307
+ {session.coreInstance ? 'Active' : 'Inactive'}
308
+ </span>
309
+ </div>
310
+
311
+ <Divider className="my-3" />
312
+
313
+ <div className="text-xs text-white/40">
314
+ <p>Settings are applied immediately and affect performance.</p>
315
+ <p className="mt-1">Lower settings recommended for older devices.</p>
316
+ </div>
317
+ </div>
318
+ </CardBody>
319
+ </Card>
320
+
321
+ {/* Reset to Defaults */}
322
+ <div className="pt-4">
323
+ <Button
324
+ variant="flat"
325
+ size="sm"
326
+ className="w-full text-white/60 hover:text-white"
327
+ onPress={async () => {
328
+ try {
329
+ await updateCoreConfig({
330
+ frameRateMode: 'full'
331
+ });
332
+ setShowFPSCounter(false);
333
+ } catch (error) {
334
+ console.error('Failed to reset settings:', error);
335
+ }
336
+ }}
337
+ >
338
+ Reset to Defaults
339
+ </Button>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ );
344
+ };
345
+
346
+ export default SettingsMenu;
@@ -0,0 +1,137 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Chip } from '@heroui/react';
3
+ import {
4
+ VideoCameraIcon,
5
+ MicrophoneIcon,
6
+ CursorArrowRaysIcon,
7
+ VideoCameraSlashIcon,
8
+ NoSymbolIcon
9
+ } from '@heroicons/react/24/outline';
10
+
11
+ interface SimpleInputControlsProps {
12
+ className?: string;
13
+ }
14
+
15
+ const SimpleInputControls: React.FC<SimpleInputControlsProps> = ({ className = '' }) => {
16
+ const [audioInput, setAudioInput] = useState<'none' | 'microphone'>('none');
17
+ const [videoInput, setVideoInput] = useState<'none' | 'camera'>('none');
18
+ const [interactionEnabled, setInteractionEnabled] = useState(true);
19
+
20
+ const handleAudioToggle = () => {
21
+ setAudioInput(audioInput === 'none' ? 'microphone' : 'none');
22
+ };
23
+
24
+ const handleVideoToggle = () => {
25
+ setVideoInput(videoInput === 'none' ? 'camera' : 'none');
26
+ };
27
+
28
+ const handleInteractionToggle = () => {
29
+ setInteractionEnabled(!interactionEnabled);
30
+ };
31
+
32
+ const getAudioIcon = () => {
33
+ return audioInput === 'microphone' ? MicrophoneIcon : NoSymbolIcon;
34
+ };
35
+
36
+ const getVideoIcon = () => {
37
+ return videoInput === 'camera' ? VideoCameraIcon : VideoCameraSlashIcon;
38
+ };
39
+
40
+ const getInteractionIcon = () => {
41
+ return CursorArrowRaysIcon;
42
+ };
43
+
44
+ return (
45
+ <div className={`flex flex-col space-y-2 pointer-events-auto ${className}`}>
46
+ {/* Audio Input Button */}
47
+ <div className="flex flex-col items-center">
48
+ <Button
49
+ isIconOnly
50
+ variant="flat"
51
+ className={`
52
+ w-12 h-12 bg-black/40 backdrop-blur-sm border border-white/20
53
+ hover:bg-white/10 transition-all duration-200
54
+ ${audioInput === 'microphone' ? 'border-primary-500 bg-primary-500/20' : ''}
55
+ `}
56
+ onPress={handleAudioToggle}
57
+ aria-label={audioInput === 'microphone' ? 'Disable microphone' : 'Enable microphone'}
58
+ >
59
+ {React.createElement(getAudioIcon(), {
60
+ className: `w-5 h-5 ${audioInput === 'microphone' ? 'text-primary-400' : 'text-white/60'}`
61
+ })}
62
+ </Button>
63
+ {audioInput === 'microphone' && (
64
+ <Chip
65
+ size="sm"
66
+ variant="flat"
67
+ color="primary"
68
+ className="text-xs mt-1"
69
+ >
70
+ Mic
71
+ </Chip>
72
+ )}
73
+ </div>
74
+
75
+ {/* Video Input Button */}
76
+ <div className="flex flex-col items-center">
77
+ <Button
78
+ isIconOnly
79
+ variant="flat"
80
+ className={`
81
+ w-12 h-12 bg-black/40 backdrop-blur-sm border border-white/20
82
+ hover:bg-white/10 transition-all duration-200
83
+ ${videoInput === 'camera' ? 'border-primary-500 bg-primary-500/20' : ''}
84
+ `}
85
+ onPress={handleVideoToggle}
86
+ aria-label={videoInput === 'camera' ? 'Disable camera' : 'Enable camera'}
87
+ >
88
+ {React.createElement(getVideoIcon(), {
89
+ className: `w-5 h-5 ${videoInput === 'camera' ? 'text-primary-400' : 'text-white/60'}`
90
+ })}
91
+ </Button>
92
+ {videoInput === 'camera' && (
93
+ <Chip
94
+ size="sm"
95
+ variant="flat"
96
+ color="primary"
97
+ className="text-xs mt-1"
98
+ >
99
+ Cam
100
+ </Chip>
101
+ )}
102
+ </div>
103
+
104
+ {/* Interaction Toggle Button */}
105
+ <div className="flex flex-col items-center">
106
+ <Button
107
+ isIconOnly
108
+ variant="flat"
109
+ className={`
110
+ w-12 h-12 bg-black/40 backdrop-blur-sm border border-white/20
111
+ hover:bg-white/10 transition-all duration-200
112
+ ${interactionEnabled ? 'border-primary-500 bg-primary-500/20' : ''}
113
+ `}
114
+ onPress={handleInteractionToggle}
115
+ aria-label={interactionEnabled ? 'Disable interactions' : 'Enable interactions'}
116
+ >
117
+ {React.createElement(getInteractionIcon(), {
118
+ className: `w-5 h-5 ${interactionEnabled ? 'text-primary-400' : 'text-white/60'}`
119
+ })}
120
+ </Button>
121
+ {interactionEnabled && (
122
+ <Chip
123
+ size="sm"
124
+ variant="flat"
125
+ color="primary"
126
+ className="text-xs mt-1"
127
+ >
128
+ Interactive
129
+ </Chip>
130
+ )}
131
+ </div>
132
+ </div>
133
+ );
134
+ };
135
+
136
+ export default SimpleInputControls;
137
+
package/src/index.css ADDED
@@ -0,0 +1,68 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './styles/globals.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )