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.
- package/.github/workflows/nextjs.yml +99 -0
- package/README.md +100 -473
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tools/assetviewer/page.d.ts +9 -4
- package/dist/tools/assetviewer/page.js +21 -17
- package/dist/tools/dragdrop/modelLoader.js +2 -3
- package/dist/tools/prefabeditor/EditorUI.d.ts +2 -1
- package/dist/tools/prefabeditor/EditorUI.js +4 -4
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +4 -1
- package/dist/tools/prefabeditor/PrefabEditor.js +4 -5
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +4 -3
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +1 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +5 -4
- package/dist/tools/prefabeditor/components/ModelComponent.js +5 -4
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
[](https://www.typescriptlang.org/)
|
|
12
11
|
[](https://react.dev/)
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
## Core Principle
|
|
15
14
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
136
|
-
enabled
|
|
137
|
-
visible
|
|
82
|
+
id: string;
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
visible?: boolean;
|
|
138
85
|
components: {
|
|
139
|
-
transform?: TransformComponent;
|
|
140
|
-
geometry?: GeometryComponent;
|
|
141
|
-
material?: MaterialComponent;
|
|
142
|
-
physics?: PhysicsComponent;
|
|
143
|
-
model?: ModelComponent;
|
|
144
|
-
// Add your own!
|
|
86
|
+
transform?: TransformComponent;
|
|
87
|
+
geometry?: GeometryComponent;
|
|
88
|
+
material?: MaterialComponent;
|
|
89
|
+
physics?: PhysicsComponent;
|
|
90
|
+
model?: ModelComponent;
|
|
145
91
|
};
|
|
146
|
-
children?: GameObject[];
|
|
92
|
+
children?: GameObject[];
|
|
147
93
|
}
|
|
148
94
|
```
|
|
149
95
|
|
|
150
|
-
|
|
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
|
-
|
|
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 |
|
|
307
|
-
|
|
308
|
-
| **Transform** | `position`, `rotation`, `scale` |
|
|
309
|
-
| **Geometry** | `geometryType`, `args` |
|
|
310
|
-
| **Material** | `color`, `texture`, `metalness`, `roughness` |
|
|
311
|
-
| **Physics** | `type
|
|
312
|
-
| **Model** | `filename`, `instanced` |
|
|
313
|
-
| **SpotLight** | `color`, `intensity`, `angle`, `penumbra` |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
143
|
+
## Implementation Details
|
|
348
144
|
|
|
349
|
-
###
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"
|
|
375
|
-
|
|
376
|
-
"
|
|
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
|
-
|
|
387
|
-
]
|
|
163
|
+
}
|
|
388
164
|
}
|
|
389
165
|
```
|
|
166
|
+
Uses drei's `<Merged>` + `<InstancedRigidBodies>` for physics. World-space transforms, terminal nodes.
|
|
390
167
|
|
|
391
|
-
###
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
Then register it:
|
|
174
|
+
### WebGPU Renderer
|
|
436
175
|
```tsx
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
464
|
-
Wrap your scene in `<Physics>`:
|
|
465
|
-
|
|
192
|
+
### Mix with React Components
|
|
466
193
|
```jsx
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<
|
|
470
|
-
<
|
|
194
|
+
<Physics>
|
|
195
|
+
<PrefabRoot data={environment} />
|
|
196
|
+
<Player />
|
|
197
|
+
<AIEnemies />
|
|
471
198
|
</Physics>
|
|
472
199
|
```
|
|
473
200
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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], //
|
|
521
|
-
"rotation": [x, y, z], //
|
|
522
|
-
"scale": [x, y, z] //
|
|
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
|
|
233
|
+
"args": [/* geometry-specific */]
|
|
530
234
|
}
|
|
531
235
|
},
|
|
532
236
|
"material": {
|
|
533
237
|
"type": "Material",
|
|
534
238
|
"properties": {
|
|
535
|
-
"color": "#rrggbb"
|
|
536
|
-
"texture": "/path/to/texture.jpg",
|
|
537
|
-
"metalness": 0.0-1.0,
|
|
538
|
-
"roughness": 0.0-1.0
|
|
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"
|
|
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
|
|
255
|
+
"instanced": true
|
|
552
256
|
}
|
|
553
257
|
}
|
|
554
258
|
},
|
|
555
|
-
"children": [/*
|
|
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
|
|
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
|
-
|
|
275
|
+
Project structure:
|
|
573
276
|
```
|
|
574
|
-
/src →
|
|
575
|
-
/shared → GameCanvas
|
|
277
|
+
/src → library source
|
|
278
|
+
/shared → GameCanvas
|
|
576
279
|
/tools
|
|
577
|
-
/prefabeditor →
|
|
578
|
-
/docs → Next.js
|
|
579
|
-
/app →
|
|
280
|
+
/prefabeditor → editor + PrefabRoot
|
|
281
|
+
/docs → Next.js site
|
|
282
|
+
/app → demo pages
|
|
580
283
|
```
|
|
581
284
|
|
|
582
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
60
|
-
fetch(
|
|
61
|
-
fetch(
|
|
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:
|
|
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:
|
|
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 -
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
-
"
|
|
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.
|
|
22
|
+
"three": "^0.181.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@react-three/drei": "^10.7.7",
|