react-three-game 0.0.3 → 0.0.5

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.
@@ -0,0 +1,99 @@
1
+ # Sample workflow for building and deploying a Next.js site to GitHub Pages
2
+ #
3
+ # To get started with Next.js see: https://nextjs.org/docs/getting-started
4
+ #
5
+ name: Deploy Next.js site to Pages
6
+
7
+ on:
8
+ # Runs on pushes targeting the default branch
9
+ push:
10
+ branches: ["main"]
11
+
12
+ # Allows you to run this workflow manually from the Actions tab
13
+ workflow_dispatch:
14
+
15
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
16
+ permissions:
17
+ contents: read
18
+ pages: write
19
+ id-token: write
20
+
21
+ # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
22
+ # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
23
+ concurrency:
24
+ group: "pages"
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ # Build job
29
+ build:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@v4
34
+ - name: Detect package manager
35
+ id: detect-package-manager
36
+ run: |
37
+ if [ -f "${{ github.workspace }}/yarn.lock" ]; then
38
+ echo "manager=yarn" >> $GITHUB_OUTPUT
39
+ echo "command=install" >> $GITHUB_OUTPUT
40
+ echo "runner=yarn" >> $GITHUB_OUTPUT
41
+ exit 0
42
+ elif [ -f "${{ github.workspace }}/package.json" ]; then
43
+ echo "manager=npm" >> $GITHUB_OUTPUT
44
+ echo "command=install" >> $GITHUB_OUTPUT
45
+ echo "runner=npx --no-install" >> $GITHUB_OUTPUT
46
+ exit 0
47
+ else
48
+ echo "Unable to determine package manager"
49
+ exit 1
50
+ fi
51
+ - name: Setup Node
52
+ uses: actions/setup-node@v4
53
+ with:
54
+ node-version: "20"
55
+ cache: ${{ steps.detect-package-manager.outputs.manager }}
56
+ - name: Setup Pages
57
+ uses: actions/configure-pages@v5
58
+ with:
59
+ # Automatically inject basePath in your Next.js configuration file and disable
60
+ # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
61
+ #
62
+ # You may remove this line if you want to manage the configuration yourself.
63
+ static_site_generator: next
64
+ - name: Restore cache
65
+ uses: actions/cache@v4
66
+ with:
67
+ path: |
68
+ docs/.next/cache
69
+ # Generate a new cache whenever packages or source files change.
70
+ key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
71
+ # If source files changed but packages didn't, rebuild from a prior cache.
72
+ restore-keys: |
73
+ ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
74
+ - name: Install root dependencies
75
+ run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
76
+ - name: Build library
77
+ run: npm run build
78
+ - name: Install docs dependencies
79
+ working-directory: ./docs
80
+ run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
81
+ - name: Build Next.js docs
82
+ working-directory: ./docs
83
+ run: ${{ steps.detect-package-manager.outputs.runner }} next build
84
+ - name: Upload artifact
85
+ uses: actions/upload-pages-artifact@v3
86
+ with:
87
+ path: ./docs/out
88
+
89
+ # Deployment job
90
+ deploy:
91
+ environment:
92
+ name: github-pages
93
+ url: ${{ steps.deployment.outputs.page_url }}
94
+ runs-on: ubuntu-latest
95
+ needs: build
96
+ steps:
97
+ - name: Deploy to GitHub Pages
98
+ id: deployment
99
+ uses: actions/deploy-pages@v4
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # react-three-game
2
2
 
3
- > **The first 3D game engine designed for AI-native development.**
4
- > Generate entire game scenes from natural language. Zero boilerplate, 100% declarative, fully typesafe.
3
+ Component-based 3D game engine where everything is JSON. Built on React Three Fiber + WebGPU.
5
4
 
6
5
  ```bash
7
6
  npm i react-three-game @react-three/fiber three
@@ -11,39 +10,11 @@ npm i react-three-game @react-three/fiber three
11
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)
12
11
  [![React](https://img.shields.io/badge/React-19-blue.svg)](https://react.dev/)
13
12
 
14
- ---
13
+ ## Core Principle
15
14
 
16
- ## 🤖 Why This Exists
17
-
18
- **Traditional 3D engines force you to write imperative code.** Unity requires C# classes. Unreal needs Blueprints. Three.js demands manual scene graph manipulation. **AI agents struggle with all of these.**
19
-
20
- **react-three-game is different:**
21
- - ✅ **Everything is JSON** - AI can generate complete scenes without writing code
22
- - ✅ **Component-based architecture** - Like Unity, but declarative and serializable
23
- - ✅ **Visual prefab editor** - Export scenes as versionable JSON files
24
- - ✅ **Built on React Three Fiber** - Leverage the entire React ecosystem
25
- - ✅ **WebGPU renderer** - Cutting-edge graphics with Three.js r181+
26
-
27
- ### The Problem We Solve
28
-
29
- ```jsx
30
- // ❌ Traditional Three.js - Imperative, verbose, AI-hostile
31
- const scene = new THREE.Scene();
32
- const geometry = new THREE.BoxGeometry();
33
- const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
34
- const cube = new THREE.Mesh(geometry, material);
35
- cube.position.set(0, 1, 0);
36
- scene.add(cube);
37
-
38
- // Physics? Even worse...
39
- const body = new CANNON.Body({ mass: 1 });
40
- body.addShape(new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)));
41
- world.addBody(body);
42
- // Now sync transforms every frame... 😱
43
- ```
15
+ Scenes are JSON prefabs. Components are registered modules. Hierarchy is declarative.
44
16
 
45
17
  ```jsx
46
- // ✅ react-three-game - Declarative, concise, AI-friendly
47
18
  <PrefabRoot data={{
48
19
  root: {
49
20
  id: "cube",
@@ -57,19 +28,12 @@ world.addBody(body);
57
28
  }} />
58
29
  ```
59
30
 
60
- **Result:** AI agents can generate this JSON structure. Version control it. Modify it. No code generation required.
61
-
62
- ---
63
-
64
- ## 🚀 Quick Start
31
+ ## Quick Start
65
32
 
66
- ### Installation
67
33
  ```bash
68
34
  npm install react-three-game @react-three/fiber @react-three/rapier three
69
35
  ```
70
36
 
71
- ### Your First Scene (30 seconds)
72
-
73
37
  ```jsx
74
38
  import { GameCanvas, PrefabRoot } from 'react-three-game';
75
39
 
@@ -81,19 +45,12 @@ export default function App() {
81
45
  id: "scene",
82
46
  root: {
83
47
  id: "root",
84
- enabled: true,
85
- visible: true,
86
48
  components: {
87
- transform: {
88
- type: "Transform",
89
- properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
90
- }
49
+ transform: { type: "Transform", properties: { position: [0, 0, 0] } }
91
50
  },
92
51
  children: [
93
52
  {
94
53
  id: "floor",
95
- enabled: true,
96
- visible: true,
97
54
  components: {
98
55
  transform: { type: "Transform", properties: { position: [0, -1, 0] } },
99
56
  geometry: { type: "Geometry", properties: { geometryType: "box", args: [10, 0.5, 10] } },
@@ -103,8 +60,6 @@ export default function App() {
103
60
  },
104
61
  {
105
62
  id: "player",
106
- enabled: true,
107
- visible: true,
108
63
  components: {
109
64
  transform: { type: "Transform", properties: { position: [0, 2, 0] } },
110
65
  geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
@@ -120,58 +75,27 @@ export default function App() {
120
75
  }
121
76
  ```
122
77
 
123
- **That's it.** Physics, rendering, transforms - all declarative. No boilerplate.
124
-
125
- ---
126
-
127
- ## 🎮 Core Concepts
128
-
129
- ### 1. GameObjects & Components
130
-
131
- Every object in your scene is a `GameObject` with modular components:
78
+ ## GameObject Structure
132
79
 
133
80
  ```typescript
134
81
  interface GameObject {
135
- id: string; // Unique identifier
136
- enabled: boolean; // Active in scene?
137
- visible: boolean; // Rendered?
82
+ id: string;
83
+ enabled?: boolean;
84
+ visible?: boolean;
138
85
  components: {
139
- transform?: TransformComponent; // Position/rotation/scale
140
- geometry?: GeometryComponent; // Box, sphere, plane, etc.
141
- material?: MaterialComponent; // Color, textures, PBR properties
142
- physics?: PhysicsComponent; // Rapier physics body
143
- model?: ModelComponent; // Load GLB/FBX models
144
- // Add your own!
86
+ transform?: TransformComponent;
87
+ geometry?: GeometryComponent;
88
+ material?: MaterialComponent;
89
+ physics?: PhysicsComponent;
90
+ model?: ModelComponent;
145
91
  };
146
- children?: GameObject[]; // Nested hierarchy
92
+ children?: GameObject[];
147
93
  }
148
94
  ```
149
95
 
150
- **This is Unity/Unreal's ECS pattern, but 100% React.**
151
-
152
- ### 2. Visual Prefab Editor
153
-
154
- Run the built-in editor:
155
- ```jsx
156
- import { PrefabEditor } from 'react-three-game';
157
-
158
- <PrefabEditor />
159
- ```
160
-
161
- - 🎨 Drag-and-drop 3D models
162
- - 🔧 Edit transforms with gizmos
163
- - 📦 Add/remove components
164
- - 💾 Export JSON files
165
- - ▶️ Toggle edit/play mode
166
-
167
- **Pro tip:** Use this to generate scenes, then let AI modify the JSON.
168
-
169
- ### 3. Component System
170
-
171
- Create custom components in minutes:
96
+ ## Custom Components
172
97
 
173
98
  ```tsx
174
- // MyLaserComponent.tsx
175
99
  import { Component } from 'react-three-game';
176
100
 
177
101
  const LaserComponent: Component = {
@@ -188,137 +112,23 @@ const LaserComponent: Component = {
188
112
  defaultProperties: { damage: 10 }
189
113
  };
190
114
 
191
- export default LaserComponent;
192
- ```
193
-
194
- Register it once, use everywhere:
195
- ```typescript
115
+ // Register
196
116
  import { registerComponent } from 'react-three-game';
197
117
  registerComponent(LaserComponent);
198
118
  ```
199
119
 
200
- ---
201
-
202
- ## 🏗️ Architecture
203
-
204
- ### GameObject Hierarchy
205
- ```
206
- Scene Root
207
- ├─ Player (Dynamic Physics)
208
- │ ├─ Camera
209
- │ └─ Weapon (Model)
210
- ├─ Enemies (Instanced)
211
- │ ├─ Enemy_01
212
- │ ├─ Enemy_02
213
- │ └─ Enemy_03
214
- └─ Environment
215
- ├─ Ground (Fixed Physics)
216
- └─ Obstacles
217
- ```
218
-
219
- ### Transform Math (Critical!)
220
- - **Local transforms** stored in JSON (relative to parent)
221
- - **World transforms** computed at runtime (for rendering)
222
- - **TransformControls** use world space, then convert back to local
223
- - Helper: `computeParentWorldMatrix()` handles the math
224
-
225
- ### Instancing System
226
- Render 1000s of objects efficiently:
227
- ```json
228
- {
229
- "components": {
230
- "model": {
231
- "type": "Model",
232
- "properties": {
233
- "filename": "tree.glb",
234
- "instanced": true // ← Magic flag
235
- }
236
- }
237
- }
238
- }
239
- ```
240
- Behind the scenes: drei's `<Merged>` + `<InstancedRigidBodies>` for physics.
241
-
242
- ---
243
-
244
- ## 🎯 Real-World Example
245
-
246
- Here's what a complete multiplayer game looks like (coming from production code):
247
-
248
- ```jsx
249
- import { GameCanvas, PrefabRoot } from 'react-three-game';
250
- import { Physics } from '@react-three/rapier';
251
-
252
- export default function Game() {
253
- return (
254
- <GameCanvas>
255
- <Physics>
256
- {/* Load entire scene from JSON */}
257
- <PrefabRoot data={levelData} />
258
-
259
- {/* Mix with React components */}
260
- <Player controllable />
261
- <Enemy position={[5, 0, -5]} />
262
- <MovingPlatform path={[[0,0,0], [10,0,0]]} />
263
- </Physics>
264
-
265
- <ambientLight intensity={0.5} />
266
- <directionalLight castShadow position={[10, 10, 5]} />
267
- </GameCanvas>
268
- );
269
- }
270
- ```
271
-
272
- **The power:** That `levelData` JSON can be:
273
- - 🤖 Generated by AI from a prompt
274
- - 🎨 Created in the visual editor
275
- - 🔄 Version controlled in git
276
- - 🌐 Loaded from a CMS
277
- - 🧩 Composed from smaller prefabs
278
-
279
- ---
280
-
281
- ## 📦 What's Included
282
-
283
- ### Core Exports
284
-
285
- ```typescript
286
- import {
287
- // Rendering
288
- GameCanvas, // WebGPU Canvas wrapper
289
- PrefabRoot, // Scene renderer from JSON
290
- PrefabEditor, // Visual editor component
291
-
292
- // Utils
293
- loadModel, // GLB/FBX loader with Draco
294
- registerComponent, // Add custom components
295
- getComponent, // Query component registry
296
-
297
- // Types
298
- Prefab, // Prefab JSON structure
299
- GameObject, // Scene node type
300
- Component, // Component interface
301
- } from 'react-three-game';
302
- ```
303
-
304
- ### Built-in Components
120
+ ## Built-in Components
305
121
 
306
- | Component | Properties | Description |
307
- |-----------|-----------|-------------|
308
- | **Transform** | `position`, `rotation`, `scale` | 3D position/orientation (always present) |
309
- | **Geometry** | `geometryType`, `args` | Box, sphere, plane, cylinder, etc. |
310
- | **Material** | `color`, `texture`, `metalness`, `roughness` | PBR materials with texture support |
311
- | **Physics** | `type` (`dynamic`/`fixed`) | Rapier rigid body |
312
- | **Model** | `filename`, `instanced` | Load GLB/FBX, toggle GPU instancing |
313
- | **SpotLight** | `color`, `intensity`, `angle`, `penumbra` | Dynamic lighting |
122
+ | Component | Properties |
123
+ |-----------|-----------|
124
+ | **Transform** | `position: [x,y,z]`, `rotation: [x,y,z]`, `scale: [x,y,z]` |
125
+ | **Geometry** | `geometryType: "box"\|"sphere"\|"plane"\|"cylinder"`, `args: number[]` |
126
+ | **Material** | `color: string`, `texture?: string`, `metalness?: number`, `roughness?: number` |
127
+ | **Physics** | `type: "dynamic"\|"fixed"` |
128
+ | **Model** | `filename: string`, `instanced?: boolean` |
129
+ | **SpotLight** | `color: string`, `intensity: number`, `angle: number`, `penumbra: number` |
314
130
 
315
- **Extending:** Create custom components in 20 lines - see "Component System" above.
316
-
317
- ---
318
-
319
- ## 🎨 Visual Editor
320
-
321
- Import and use the prefab editor:
131
+ ## Prefab Editor
322
132
 
323
133
  ```jsx
324
134
  import { PrefabEditor } from 'react-three-game';
@@ -328,337 +138,154 @@ export default function EditorPage() {
328
138
  }
329
139
  ```
330
140
 
331
- ### Editor Features
332
- - **📥 Import/Export** - Load/save JSON prefabs
333
- - **🎮 Edit/Play Toggle** - Test physics in real-time
334
- - **🔧 Transform Gizmos** - Translate/Rotate/Scale (T/R/S keys)
335
- - **🌳 Scene Tree** - Drag to reorder, click to select
336
- - **📋 Inspector** - Edit component properties
337
- - **➕ Add Components** - Dropdown to attach new behaviors
338
-
339
- ### Workflow
340
- 1. Create scene in editor
341
- 2. Export JSON
342
- 3. Load in game: `<PrefabRoot data={require('./level1.json')} />`
343
- 4. Or generate variations with AI by modifying the JSON
344
-
345
- ---
141
+ Transform gizmos (T/R/S keys), drag-to-reorder tree, import/export JSON, edit/play toggle.
346
142
 
347
- ## 🚀 Advanced Patterns
143
+ ## Implementation Details
348
144
 
349
- ### Loading External Prefabs
350
-
351
- ```jsx
352
- import levelData from './prefabs/arena.json';
353
-
354
- <PrefabRoot data={levelData} />
355
- ```
356
-
357
- ### Mixing Prefabs with React Components
358
-
359
- ```jsx
360
- <Physics>
361
- <PrefabRoot data={environment} /> {/* Static level geometry */}
362
- <Player /> {/* Dynamic player logic */}
363
- <AIEnemies /> {/* Procedural spawning */}
364
- </Physics>
365
- ```
366
-
367
- ### Dynamic Instancing (1000+ Objects)
145
+ ### Transform Hierarchy
146
+ - Local transforms stored in JSON (relative to parent)
147
+ - World transforms computed at runtime via matrix multiplication
148
+ - `computeParentWorldMatrix(root, targetId)` traverses tree for parent's world matrix
149
+ - TransformControls extract world matrix → compute parent inverse → derive new local transform
368
150
 
151
+ ### GPU Instancing
152
+ Enable with `model.properties.instanced = true`:
369
153
  ```json
370
154
  {
371
- "id": "forest",
372
- "children": [
373
- {
374
- "id": "tree-1",
375
- "components": {
376
- "model": {
377
- "type": "Model",
378
- "properties": {
379
- "filename": "tree.glb",
380
- "instanced": true // ← Automatic GPU instancing
381
- }
382
- },
383
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
155
+ "components": {
156
+ "model": {
157
+ "type": "Model",
158
+ "properties": {
159
+ "filename": "tree.glb",
160
+ "instanced": true
384
161
  }
385
162
  }
386
- // Repeat 1000x - only renders once internally
387
- ]
163
+ }
388
164
  }
389
165
  ```
166
+ Uses drei's `<Merged>` + `<InstancedRigidBodies>` for physics. World-space transforms, terminal nodes.
390
167
 
391
- ### Custom Components (Real Example)
392
-
393
- ```tsx
394
- // LaserBeamComponent.tsx
395
- import { Component } from 'react-three-game';
396
- import { Line } from '@react-three/drei';
397
-
398
- const LaserBeam: Component = {
399
- name: 'LaserBeam',
400
-
401
- Editor: ({ component, onUpdate }) => (
402
- <>
403
- <label>Damage</label>
404
- <input
405
- type="number"
406
- value={component.properties.damage}
407
- onChange={e => onUpdate({ damage: +e.target.value })}
408
- />
409
- <label>Color</label>
410
- <input
411
- type="color"
412
- value={component.properties.color}
413
- onChange={e => onUpdate({ color: e.target.value })}
414
- />
415
- </>
416
- ),
417
-
418
- View: ({ properties }) => (
419
- <Line
420
- points={[[0, 0, 0], [0, 0, -10]]}
421
- color={properties.color}
422
- lineWidth={3}
423
- />
424
- ),
425
-
426
- defaultProperties: {
427
- damage: 25,
428
- color: '#ff0000'
429
- }
430
- };
168
+ ### Model Loading
169
+ - Supports GLB/GLTF (Draco compression) and FBX
170
+ - Singleton loaders in `modelLoader.ts`
171
+ - Draco decoder from `https://www.gstatic.com/draco/v1/decoders/`
172
+ - Auto-loads when `model.properties.filename` detected
431
173
 
432
- export default LaserBeam;
433
- ```
434
-
435
- Then register it:
174
+ ### WebGPU Renderer
436
175
  ```tsx
437
- import { registerComponent } from 'react-three-game';
438
- import LaserBeam from './components/LaserBeam';
439
-
440
- registerComponent(LaserBeam);
176
+ <Canvas gl={async ({ canvas }) => {
177
+ const renderer = new WebGPURenderer({ canvas, shadowMap: true });
178
+ await renderer.init(); // Required
179
+ return renderer;
180
+ }}>
441
181
  ```
182
+ Use `MeshStandardNodeMaterial` not `MeshStandardMaterial`.
442
183
 
443
- Now it's available in the editor dropdown AND can be serialized in prefab JSON!
444
-
445
- ---
446
-
447
- ## 🤝 Integrations
448
-
449
- ### React Three Fiber Ecosystem
450
- All `@react-three/drei` helpers work seamlessly:
184
+ ## Patterns
451
185
 
186
+ ### Load External Prefabs
452
187
  ```jsx
453
- import { OrbitControls, Sky, ContactShadows } from '@react-three/drei';
454
-
455
- <GameCanvas>
456
- <Sky />
457
- <OrbitControls />
458
- <PrefabRoot data={scene} />
459
- <ContactShadows />
460
- </GameCanvas>
188
+ import levelData from './prefabs/arena.json';
189
+ <PrefabRoot data={levelData} />
461
190
  ```
462
191
 
463
- ### Physics (@react-three/rapier)
464
- Wrap your scene in `<Physics>`:
465
-
192
+ ### Mix with React Components
466
193
  ```jsx
467
- import { Physics } from '@react-three/rapier';
468
-
469
- <Physics gravity={[0, -9.8, 0]}>
470
- <PrefabRoot data={level} />
194
+ <Physics>
195
+ <PrefabRoot data={environment} />
196
+ <Player />
197
+ <AIEnemies />
471
198
  </Physics>
472
199
  ```
473
200
 
474
- Components with `physics` property automatically get rigid bodies.
475
-
476
- ---
477
-
478
- ## 🎯 For AI Agents
479
-
480
- ### Prompt Templates
481
-
482
- **Generate a complete scene:**
483
- ```
484
- Create a react-three-game prefab JSON for a platformer level with:
485
- - A ground plane (10x10, fixed physics, grass texture)
486
- - 5 floating platforms (dynamic physics)
487
- - A player spawn point at [0, 5, 0]
488
- - 3 collectible coins using sphere geometry
489
- ```
490
-
491
- **Modify existing scenes:**
492
- ```
493
- Take this prefab JSON and add:
494
- - A spotlight pointing at the player spawn
495
- - Convert all "box" geometry to "sphere"
496
- - Scale all objects by 1.5x
497
- ```
498
-
499
- **Generate component variations:**
500
- ```
501
- Create 10 enemy prefab variants by:
502
- - Randomizing position within bounds [[-10,10], [0,5], [-10,10]]
503
- - Varying scale from 0.8 to 1.2
504
- - Using colors from palette: ["#ff6b6b", "#ee5a6f", "#c44569"]
201
+ ### Update Prefab Nodes
202
+ ```typescript
203
+ function updatePrefabNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject {
204
+ if (root.id === id) return update(root);
205
+ if (root.children) {
206
+ return { ...root, children: root.children.map(child => updatePrefabNode(child, id, update)) };
207
+ }
208
+ return root;
209
+ }
505
210
  ```
506
211
 
507
- ### JSON Structure Reference
212
+ ## AI Agent Reference
508
213
 
214
+ ### Prefab JSON Schema
509
215
  ```typescript
510
216
  {
511
217
  "id": "unique-id",
512
218
  "root": {
513
219
  "id": "root-id",
514
- "enabled": true,
515
- "visible": true,
516
220
  "components": {
517
221
  "transform": {
518
222
  "type": "Transform",
519
223
  "properties": {
520
- "position": [x, y, z], // Numbers in world units
521
- "rotation": [x, y, z], // Radians
522
- "scale": [x, y, z] // Multipliers
224
+ "position": [x, y, z], // world units
225
+ "rotation": [x, y, z], // radians
226
+ "scale": [x, y, z] // multipliers
523
227
  }
524
228
  },
525
229
  "geometry": {
526
230
  "type": "Geometry",
527
231
  "properties": {
528
232
  "geometryType": "box" | "sphere" | "plane" | "cylinder" | "cone" | "torus",
529
- "args": [/* geometry-specific arguments */]
233
+ "args": [/* geometry-specific */]
530
234
  }
531
235
  },
532
236
  "material": {
533
237
  "type": "Material",
534
238
  "properties": {
535
- "color": "#rrggbb" | "colorname",
536
- "texture": "/path/to/texture.jpg", // Optional
537
- "metalness": 0.0-1.0, // Optional
538
- "roughness": 0.0-1.0 // Optional
239
+ "color": "#rrggbb",
240
+ "texture": "/path/to/texture.jpg",
241
+ "metalness": 0.0-1.0,
242
+ "roughness": 0.0-1.0
539
243
  }
540
244
  },
541
245
  "physics": {
542
246
  "type": "Physics",
543
247
  "properties": {
544
- "type": "dynamic" | "fixed" // Dynamic = moves, Fixed = static
248
+ "type": "dynamic" | "fixed"
545
249
  }
546
250
  },
547
251
  "model": {
548
252
  "type": "Model",
549
253
  "properties": {
550
254
  "filename": "/models/asset.glb",
551
- "instanced": true // Optional: GPU instancing
255
+ "instanced": true
552
256
  }
553
257
  }
554
258
  },
555
- "children": [/* Recursive GameObject array */]
259
+ "children": [/* recursive GameObjects */]
556
260
  }
557
261
  }
558
262
  ```
559
263
 
560
- ---
561
-
562
- ## 🛠️ Development
264
+ ## Development
563
265
 
564
- ### Local Setup
565
266
  ```bash
566
267
  git clone https://github.com/prnthh/react-three-game.git
567
268
  cd react-three-game
568
269
  npm install
569
- npm run dev # Runs tsc --watch + Next.js docs site
270
+ npm run dev # tsc --watch + Next.js docs
271
+ npm run build # TypeScript → /dist
272
+ npm publish # publish to npm
570
273
  ```
571
274
 
572
- ### Project Structure
275
+ Project structure:
573
276
  ```
574
- /src → Library source (exports to npm)
575
- /shared → GameCanvas (WebGPU wrapper)
277
+ /src → library source
278
+ /shared → GameCanvas
576
279
  /tools
577
- /prefabeditor → Visual editor + PrefabRoot renderer
578
- /docs → Next.js documentation site
579
- /app → Demo pages
280
+ /prefabeditor → editor + PrefabRoot
281
+ /docs → Next.js site
282
+ /app → demo pages
580
283
  ```
581
284
 
582
- ### Building
583
- ```bash
584
- npm run build # Compile TypeScript → /dist
585
- npm publish # Publish to npm
586
- ```
587
-
588
- ---
589
-
590
- ## 🌟 Roadmap
285
+ ## Tech Stack
591
286
 
592
- - [x] Core prefab system
593
- - [x] Visual editor
594
- - [x] Component registry
595
- - [x] GPU instancing
596
- - [x] Physics integration
597
- - [ ] Input system (keyboard/gamepad/touch) - **Coming Soon**
598
- - [ ] Multiplayer primitives (WebRTC sync)
599
- - [ ] Animation system (state machines)
600
- - [ ] Audio components (spatial sound)
601
- - [ ] Particle effects
602
- - [ ] AI behavior trees (as JSON!)
287
+ React 19 Three.js r181 • TypeScript 5 • WebGPU • Rapier Physics
603
288
 
604
- ---
605
-
606
- ## 🤖 Why Developers AND AI Love This
607
-
608
- ### For Developers
609
- - ✅ **Skip the boilerplate** - No manual scene graph management
610
- - ✅ **React patterns** - Use hooks, context, state like normal React
611
- - ✅ **Visual debugging** - See and edit your scene in real-time
612
- - ✅ **Type safety** - Full TypeScript support
613
- - ✅ **Hot reload** - Changes reflect instantly
614
-
615
- ### For AI Agents
616
- - ✅ **Pure data** - No imperative code to generate
617
- - ✅ **JSON schema** - Structured, validatable format
618
- - ✅ **Compositional** - Build complex scenes from simple primitives
619
- - ✅ **Version controllable** - Git-friendly text files
620
- - ✅ **Deterministic** - Same JSON = same scene every time
621
-
622
- ---
623
-
624
- ## 📚 Examples
625
-
626
- Check out `/docs/app/demo` for live examples:
627
- - Basic scene with physics
628
- - Model loading and instancing
629
- - Custom component creation
630
- - Multiplayer game prototype
631
-
632
- ---
633
-
634
- ## 🤝 Contributing
635
-
636
- This is an **AI-first** project. We welcome:
637
- - New built-in components
638
- - Documentation improvements
639
- - Example scenes/games
640
- - AI prompt templates
641
- - Bug reports
642
-
643
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
644
-
645
- ---
646
-
647
- ## 📄 License
289
+ ## License
648
290
 
649
291
  MIT © [prnth](https://github.com/prnthh)
650
-
651
- ---
652
-
653
- ## 💬 Community
654
-
655
- - 🐦 Twitter: [@prnth](https://twitter.com/prnth)
656
- - 💼 GitHub Issues: [Report bugs](https://github.com/prnthh/react-three-game/issues)
657
- - 💡 Discussions: [Share ideas](https://github.com/prnthh/react-three-game/discussions)
658
-
659
- ---
660
-
661
- **Built with:** React 19 • Three.js r181 • TypeScript 5 • WebGPU • Rapier Physics
662
-
663
- **Status:** Alpha v0.0.1 - API may change
664
- **AI Prompt to Share:** "Build me a 3D game using react-three-game with [your features]"
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { default as GameCanvas } from './shared/GameCanvas';
2
2
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
3
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
4
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
5
+ export { default as AssetViewerPage, TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas } from './tools/assetviewer/page';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { default as GameCanvas } from './shared/GameCanvas';
2
2
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
3
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
4
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
5
+ export { default as AssetViewerPage, TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas } from './tools/assetviewer/page';
@@ -1,21 +1,26 @@
1
- export default function AssetViewerPage(): import("react/jsx-runtime").JSX.Element;
1
+ export default function AssetViewerPage({ basePath }?: {
2
+ basePath?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
2
4
  interface TextureListViewerProps {
3
5
  files: string[];
4
6
  selected?: string;
5
7
  onSelect: (file: string) => void;
8
+ basePath?: string;
6
9
  }
7
- export declare function TextureListViewer({ files, selected, onSelect }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function TextureListViewer({ files, selected, onSelect, basePath }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
8
11
  interface ModelListViewerProps {
9
12
  files: string[];
10
13
  selected?: string;
11
14
  onSelect: (file: string) => void;
15
+ basePath?: string;
12
16
  }
13
- export declare function ModelListViewer({ files, selected, onSelect }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function ModelListViewer({ files, selected, onSelect, basePath }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
14
18
  interface SoundListViewerProps {
15
19
  files: string[];
16
20
  selected?: string;
17
21
  onSelect: (file: string) => void;
22
+ basePath?: string;
18
23
  }
19
- export declare function SoundListViewer({ files, selected, onSelect }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
20
25
  export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
21
26
  export {};
@@ -49,27 +49,28 @@ function useInView() {
49
49
  }, []);
50
50
  return { ref, isInView };
51
51
  }
52
- export default function AssetViewerPage() {
52
+ export default function AssetViewerPage({ basePath = "" } = {}) {
53
53
  const [textures, setTextures] = useState([]);
54
54
  const [models, setModels] = useState([]);
55
55
  const [sounds, setSounds] = useState([]);
56
56
  const [loading, setLoading] = useState(true);
57
57
  useEffect(() => {
58
+ const base = basePath ? `${basePath}/` : '';
58
59
  Promise.all([
59
- fetch('/textures/manifest.json').then(r => r.json()),
60
- fetch('/models/manifest.json').then(r => r.json()),
61
- fetch('/sound/manifest.json').then(r => r.json()).catch(() => [])
60
+ fetch(`/${base}textures/manifest.json`).then(r => r.json()),
61
+ fetch(`/${base}models/manifest.json`).then(r => r.json()),
62
+ fetch(`/${base}sound/manifest.json`).then(r => r.json()).catch(() => [])
62
63
  ]).then(([textureData, modelData, soundData]) => {
63
64
  setTextures(textureData);
64
65
  setModels(modelData);
65
66
  setSounds(soundData);
66
67
  setLoading(false);
67
68
  });
68
- }, []);
69
+ }, [basePath]);
69
70
  if (loading) {
70
71
  return _jsx("div", { className: "p-4 text-gray-300", children: "Loading manifests..." });
71
72
  }
72
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "p-2 text-gray-300 overflow-y-auto h-screen text-sm", children: [_jsx("h1", { className: "text-lg mb-2 font-bold", children: "Asset Viewer" }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Textures (", textures.length, ")"] }), _jsx(TextureListViewer, { files: textures, onSelect: (file) => console.log('Selected texture:', file) }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Models (", models.length, ")"] }), _jsx(ModelListViewer, { files: models, onSelect: (file) => console.log('Selected model:', file) }), sounds.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Sounds (", sounds.length, ")"] }), _jsx(SoundListViewer, { files: sounds, onSelect: (file) => console.log('Selected sound:', file) })] }))] }), _jsx(SharedCanvas, {})] }));
73
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "p-2 text-gray-300 overflow-y-auto h-screen text-sm", children: [_jsx("h1", { className: "text-lg mb-2 font-bold", children: "Asset Viewer" }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Textures (", textures.length, ")"] }), _jsx(TextureListViewer, { files: textures, basePath: basePath, onSelect: (file) => console.log('Selected texture:', file) }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Models (", models.length, ")"] }), _jsx(ModelListViewer, { files: models, basePath: basePath, onSelect: (file) => console.log('Selected model:', file) }), sounds.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Sounds (", sounds.length, ")"] }), _jsx(SoundListViewer, { files: sounds, basePath: basePath, onSelect: (file) => console.log('Selected sound:', file) })] }))] }), _jsx(SharedCanvas, {})] }));
73
74
  }
74
75
  function AssetListViewer({ files, selected, onSelect, renderCard }) {
75
76
  const [currentPath, setCurrentPath] = useState('');
@@ -89,17 +90,18 @@ function AssetListViewer({ files, selected, onSelect, renderCard }) {
89
90
  setShowPicker(false);
90
91
  }) }, file)))] })] }));
91
92
  }
92
- export function TextureListViewer({ files, selected, onSelect }) {
93
- return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
93
+ export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
94
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
94
95
  }
95
- function TextureCard({ file, onSelect }) {
96
+ function TextureCard({ file, onSelect, basePath = "" }) {
96
97
  const [error, setError] = useState(false);
97
98
  const [isHovered, setIsHovered] = useState(false);
98
99
  const { ref, isInView } = useInView();
100
+ const fullPath = basePath ? `/${basePath}${file}` : file;
99
101
  if (error) {
100
102
  return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
101
103
  }
102
- return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col", onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: file, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
104
+ return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col", onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
103
105
  }
104
106
  function TextureSphere({ url, onError }) {
105
107
  const texture = useLoader(TextureLoader, url, undefined, (error) => {
@@ -108,16 +110,17 @@ function TextureSphere({ url, onError }) {
108
110
  });
109
111
  return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
110
112
  }
111
- export function ModelListViewer({ files, selected, onSelect }) {
112
- return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
113
+ export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
114
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
113
115
  }
114
- function ModelCard({ file, onSelect }) {
116
+ function ModelCard({ file, onSelect, basePath = "" }) {
115
117
  const [error, setError] = useState(false);
116
118
  const { ref, isInView } = useInView();
119
+ const fullPath = basePath ? `/${basePath}${file}` : file;
117
120
  if (error) {
118
121
  return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
119
122
  }
120
- return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col", onClick: () => onSelect(file), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: file, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
123
+ return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col", onClick: () => onSelect(file), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
121
124
  }
122
125
  function ModelPreview({ url, onError }) {
123
126
  const isFbx = url.toLowerCase().endsWith('.fbx');
@@ -133,11 +136,12 @@ function FBXModel({ url, onError }) {
133
136
  const fbx = useFBX(url);
134
137
  return _jsx("primitive", { object: fbx, scale: 0.01 });
135
138
  }
136
- export function SoundListViewer({ files, selected, onSelect }) {
137
- return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, onSelect: onSelectHandler })) }));
139
+ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
140
+ return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }));
138
141
  }
139
- function SoundCard({ file, onSelect }) {
142
+ function SoundCard({ file, onSelect, basePath = "" }) {
140
143
  const fileName = file.split('/').pop() || '';
144
+ const fullPath = basePath ? `/${basePath}${file}` : file;
141
145
  return (_jsxs("div", { onClick: () => onSelect(file), className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex flex-col items-center justify-center", children: [_jsx("div", { className: "text-2xl", children: "\uD83D\uDD0A" }), _jsx("div", { className: "text-[10px] px-1 mt-1 truncate text-center w-full", children: fileName })] }));
142
146
  }
143
147
  // Shared Canvas Component - can be used independently in any viewer
@@ -17,9 +17,8 @@ const fbxLoader = new FBXLoader();
17
17
  export function loadModel(filename_1) {
18
18
  return __awaiter(this, arguments, void 0, function* (filename, resourcePath = "", onProgress) {
19
19
  try {
20
- // Construct full path - always prepend resourcePath if provided (even if empty string)
21
- // This allows loading from root with resourcePath=""
22
- const fullPath = `${resourcePath}/${filename}`;
20
+ // Construct full path - handle empty resourcePath to avoid double slashes
21
+ const fullPath = resourcePath ? `${resourcePath}/${filename}` : filename;
23
22
  if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
24
23
  return new Promise((resolve) => {
25
24
  gltfLoader.load(fullPath, (gltf) => resolve({ success: true, model: gltf.scene }), (progressEvent) => {
@@ -1,11 +1,12 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }: {
3
+ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
7
7
  setSelectedId: Dispatch<SetStateAction<string | null>>;
8
8
  transformMode: "translate" | "rotate" | "scale";
9
9
  setTransformMode: (m: "translate" | "rotate" | "scale") => void;
10
+ basePath?: string;
10
11
  }): import("react/jsx-runtime").JSX.Element;
11
12
  export default EditorUI;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect } from 'react';
3
3
  import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
- function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }) {
5
+ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
6
6
  const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
7
7
  const updateNode = (updater) => {
8
8
  if (!prefabData || !setPrefabData || !selectedId)
@@ -24,9 +24,9 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
24
24
  };
25
25
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
26
26
  // if (!selectedNode) return null;
27
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
27
+ return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
28
28
  }
29
- function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode }) {
29
+ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
30
30
  const ALL_COMPONENTS = getAllComponents();
31
31
  const allComponentKeys = Object.keys(ALL_COMPONENTS);
32
32
  const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
@@ -50,7 +50,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
50
50
  const components = Object.assign({}, n.components);
51
51
  delete components[key];
52
52
  return Object.assign(Object.assign({}, n), { components });
53
- }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))) })) : null] }, key));
53
+ }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
54
54
  }), _jsxs("div", { className: "px-1.5 py-1 border-t border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Add Component" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("select", { className: "bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { className: "bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30", disabled: !addComponentType, onClick: () => {
55
55
  var _a;
56
56
  if (!addComponentType)
@@ -1,4 +1,7 @@
1
- declare const PrefabEditor: ({ children }: {
1
+ import { Prefab } from "./types";
2
+ declare const PrefabEditor: ({ basePath, initialPrefab, children }: {
3
+ basePath?: string;
4
+ initialPrefab?: Prefab;
2
5
  children?: React.ReactNode;
3
6
  }) => import("react/jsx-runtime").JSX.Element;
4
7
  export default PrefabEditor;
@@ -13,11 +13,10 @@ import GameCanvas from "../../shared/GameCanvas";
13
13
  import { useState, useRef, } from "react";
14
14
  import PrefabRoot from "./PrefabRoot";
15
15
  import { Physics } from "@react-three/rapier";
16
- // import testPrefab from "./samples/test.json";
17
16
  import EditorUI from "./EditorUI";
18
- const PrefabEditor = ({ children }) => {
17
+ const PrefabEditor = ({ basePath, initialPrefab, children }) => {
19
18
  const [editMode, setEditMode] = useState(true);
20
- const [loadedPrefab, setLoadedPrefab] = useState({
19
+ const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
21
20
  "id": "prefab-default",
22
21
  "name": "New Prefab",
23
22
  "root": {
@@ -41,11 +40,11 @@ const PrefabEditor = ({ children }) => {
41
40
  const prefabRef = useRef(null);
42
41
  return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
43
42
  // props for edit mode
44
- editMode: editMode, onPrefabChange: setLoadedPrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
43
+ editMode: editMode, onPrefabChange: setLoadedPrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
45
44
  const prefab = yield loadJson();
46
45
  if (prefab)
47
46
  setLoadedPrefab(prefab);
48
- }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: setLoadedPrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode })] });
47
+ }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: setLoadedPrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
49
48
  };
50
49
  const saveJson = (data, filename) => {
51
50
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -8,5 +8,6 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
8
8
  onSelect?: (id: string | null) => void;
9
9
  transformMode?: "translate" | "rotate" | "scale";
10
10
  setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
11
+ basePath?: string;
11
12
  } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
12
13
  export default PrefabRoot;
@@ -28,7 +28,7 @@ function updatePrefabNode(root, id, update) {
28
28
  }
29
29
  return root;
30
30
  }
31
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode }, ref) => {
31
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode, basePath = "" }, ref) => {
32
32
  const [loadedModels, setLoadedModels] = useState({});
33
33
  const [loadedTextures, setLoadedTextures] = useState({});
34
34
  // const [prefabRoot, setPrefabRoot] = useState<Prefab>(data); // Removed local state
@@ -108,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
108
108
  for (const filename of modelsToLoad) {
109
109
  if (!loadedModels[filename] && !loadingRefs.current.has(filename)) {
110
110
  loadingRefs.current.add(filename);
111
- const result = yield loadModel(filename, "");
111
+ const result = yield loadModel(filename, basePath);
112
112
  if (result.success && result.model) {
113
113
  setLoadedModels(prev => (Object.assign(Object.assign({}, prev), { [filename]: result.model })));
114
114
  }
@@ -118,7 +118,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
118
118
  for (const filename of texturesToLoad) {
119
119
  if (!loadedTextures[filename] && !loadingRefs.current.has(filename)) {
120
120
  loadingRefs.current.add(filename);
121
- textureLoader.load(filename, (texture) => {
121
+ const texturePath = basePath ? `${basePath}/${filename}` : filename;
122
+ textureLoader.load(texturePath, (texture) => {
122
123
  texture.colorSpace = SRGBColorSpace;
123
124
  setLoadedTextures(prev => (Object.assign(Object.assign({}, prev), { [filename]: texture })));
124
125
  });
@@ -4,6 +4,7 @@ export interface Component {
4
4
  Editor: FC<{
5
5
  component: any;
6
6
  onUpdate: (newComp: any) => void;
7
+ basePath?: string;
7
8
  }>;
8
9
  defaultProperties: any;
9
10
  View?: FC<any>;
@@ -1,16 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TextureListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
- function MaterialComponentEditor({ component, onUpdate }) {
4
+ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
5
5
  var _a, _b, _c, _d;
6
6
  const [textureFiles, setTextureFiles] = useState([]);
7
7
  useEffect(() => {
8
- fetch('/textures/manifest.json')
8
+ const base = basePath ? `${basePath}/` : '';
9
+ fetch(`/${base}textures/manifest.json`)
9
10
  .then(r => r.json())
10
11
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
11
12
  .catch(console.error);
12
- }, []);
13
- return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Texture" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }) }) })] }), component.properties.texture && (_jsxs("div", { className: "border-t border-cyan-500/20 pt-1 mt-1", children: [_jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Repeat (X, Y)" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
13
+ }, [basePath]);
14
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Texture" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { className: "border-t border-cyan-500/20 pt-1 mt-1", children: [_jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Repeat (X, Y)" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
14
15
  var _a, _b;
15
16
  const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
16
17
  onUpdate({ 'repeatCount': [parseFloat(e.target.value), y] });
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
- function ModelComponentEditor({ component, onUpdate }) {
4
+ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
5
5
  const [modelFiles, setModelFiles] = useState([]);
6
6
  useEffect(() => {
7
- fetch('/models/manifest.json')
7
+ const base = basePath ? `${basePath}/` : '';
8
+ fetch(`/${base}models/manifest.json`)
8
9
  .then(r => r.json())
9
10
  .then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
10
11
  .catch(console.error);
11
- }, []);
12
+ }, [basePath]);
12
13
  const handleModelSelect = (file) => {
13
14
  // Remove leading slash for prefab compatibility
14
15
  const filename = file.startsWith('/') ? file.slice(1) : file;
15
16
  onUpdate({ 'filename': filename });
16
17
  };
17
- return _jsxs("div", { children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Model" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), className: "w-3 h-3" }), _jsx("label", { htmlFor: "instanced-checkbox", className: "text-[9px] text-cyan-400/60", children: "Instanced" })] })] });
18
+ return _jsxs("div", { children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Model" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), className: "w-3 h-3" }), _jsx("label", { htmlFor: "instanced-checkbox", className: "text-[9px] text-cyan-400/60", children: "Instanced" })] })] });
18
19
  }
19
20
  // View for Model component
20
21
  function ModelComponentView({ properties, loadedModels, children }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -9,7 +9,7 @@
9
9
  "watch": "tsc --watch",
10
10
  "dev": "concurrently \"npm run watch\" \"cd docs && npm run dev\"",
11
11
  "build": "tsc",
12
- "publish": "npm run build && npm publish --access public"
12
+ "release": "npm run build && npm publish --access public"
13
13
  },
14
14
  "keywords": [],
15
15
  "author": "prnth",
@@ -19,7 +19,7 @@
19
19
  "@react-three/fiber": "^9.0.0",
20
20
  "react": "^18.0.0 || ^19.0.0",
21
21
  "react-dom": "^18.0.0 || ^19.0.0",
22
- "three": "^0.160.0"
22
+ "three": "^0.181.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@react-three/drei": "^10.7.7",