react-three-game 0.0.61 → 0.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,17 +161,73 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
161
161
  ## Prefab Editor
162
162
 
163
163
  ```jsx
164
+ import { useRef } from 'react';
164
165
  import { PrefabEditor } from 'react-three-game';
165
166
 
166
167
  // Standalone editor
167
168
  <PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
168
169
 
170
+ // Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
171
+ <PrefabEditor initialPrefab={sceneData} showUI={false} />
172
+
169
173
  // With custom R3F components
170
174
  <PrefabEditor initialPrefab={sceneData}>
171
175
  <CustomComponent />
172
176
  </PrefabEditor>
173
177
  ```
174
178
 
179
+ ### Embedded / Headless Editor
180
+
181
+ ```tsx
182
+ import { useRef } from 'react';
183
+ import type { Object3D } from 'three';
184
+ import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
185
+
186
+ export function EmbeddedEditor({ prefab, onPrefabChange }: {
187
+ prefab: any;
188
+ onPrefabChange: (nextPrefab: any) => void;
189
+ }) {
190
+ const editorRef = useRef<PrefabEditorRef>(null);
191
+
192
+ function loadScene(nextPrefab: any) {
193
+ editorRef.current?.replacePrefab(nextPrefab);
194
+ }
195
+
196
+ function importRuntimeModel(model: Object3D) {
197
+ editorRef.current?.addModel('models/runtime/chair.glb', model, {
198
+ name: 'Chair',
199
+ parentId: 'root',
200
+ });
201
+ }
202
+
203
+ return (
204
+ <div style={{ position: 'relative', height: 600 }}>
205
+ <div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
206
+ <button onClick={() => loadScene(prefab)}>Reload Scene</button>
207
+ <button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
208
+ </div>
209
+
210
+ <PrefabEditor
211
+ ref={editorRef}
212
+ initialPrefab={prefab}
213
+ onPrefabChange={onPrefabChange}
214
+ showUI={false}
215
+ physics={false}
216
+ enableWindowDrop={false}
217
+ />
218
+ </div>
219
+ );
220
+ }
221
+ ```
222
+
223
+ `showUI={false}` hides the built-in editor chrome but keeps canvas selection, transform controls, and scene interaction. For embedded tools, use the editor ref instead of reaching through `rootRef`:
224
+
225
+ - `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
226
+ - `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
227
+ - `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
228
+ - `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
229
+ - `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
230
+
175
231
  Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
176
232
 
177
233
  Editor menu structure:
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { registerComponent } from './tools/prefabeditor/components/ComponentRegi
7
7
  export { FieldRenderer, FieldGroup, Input, Label, Vector3Input, Vector3Field, NumberField, ColorInput, ColorField, StringInput, StringField, BooleanInput, BooleanField, SelectInput, SelectField, } from './tools/prefabeditor/components/Input';
8
8
  export * from './tools/prefabeditor/utils';
9
9
  export type { ExportGLBOptions } from './tools/prefabeditor/utils';
10
- export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
10
+ export type { PrefabEditorAssetOptions, PrefabEditorProps, PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
11
11
  export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
12
12
  export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
13
13
  export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
@@ -4,6 +4,7 @@ interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
4
4
  loader?: boolean;
5
5
  children: React.ReactNode;
6
6
  glConfig?: WebGPURendererParameters;
7
+ canvasRef?: React.RefObject<HTMLCanvasElement | null>;
7
8
  }
8
- export default function GameCanvas({ loader, children, glConfig, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
9
+ export default function GameCanvas({ loader, children, glConfig, canvasRef, onCreated, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
9
10
  export {};
@@ -31,7 +31,7 @@ extend({
31
31
  SpriteNodeMaterial: SpriteNodeMaterial,
32
32
  });
33
33
  export default function GameCanvas(_a) {
34
- var { loader = false, children, glConfig } = _a, props = __rest(_a, ["loader", "children", "glConfig"]);
34
+ var { loader = false, children, glConfig, canvasRef, onCreated } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated"]);
35
35
  const [frameloop, setFrameloop] = useState("never");
36
36
  return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: { touchAction: 'none', userSelect: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
37
37
  const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
@@ -41,5 +41,10 @@ export default function GameCanvas(_a) {
41
41
  setFrameloop("always");
42
42
  });
43
43
  return renderer;
44
- }) }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
44
+ }), onCreated: (state) => {
45
+ if (canvasRef) {
46
+ canvasRef.current = state.gl.domElement;
47
+ }
48
+ onCreated === null || onCreated === void 0 ? void 0 : onCreated(state);
49
+ } }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
45
50
  }
@@ -1,18 +1,32 @@
1
- import { Prefab } from "./types";
1
+ import { Object3D, Texture } from "three";
2
+ import { GameObject, Prefab } from "./types";
2
3
  import { PrefabRootRef } from "./PrefabRoot";
4
+ import type { ExportGLBOptions } from "./utils";
5
+ export interface PrefabEditorAssetOptions {
6
+ name?: string;
7
+ parentId?: string;
8
+ select?: boolean;
9
+ }
3
10
  export interface PrefabEditorRef {
4
11
  screenshot: () => void;
5
- exportGLB: () => void;
12
+ exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | object | undefined>;
13
+ exportGLBData: () => Promise<ArrayBuffer | undefined>;
6
14
  prefab: Prefab;
7
15
  setPrefab: (prefab: Prefab) => void;
16
+ replacePrefab: (prefab: Prefab) => void;
17
+ addModel: (path: string, model: Object3D, options?: PrefabEditorAssetOptions) => GameObject;
18
+ addTexture: (path: string, texture: Texture, options?: PrefabEditorAssetOptions) => GameObject;
8
19
  rootRef: React.RefObject<PrefabRootRef | null>;
9
20
  }
10
- declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
21
+ export interface PrefabEditorProps {
11
22
  basePath?: string;
12
23
  initialPrefab?: Prefab;
13
24
  physics?: boolean;
14
25
  onPrefabChange?: (prefab: Prefab) => void;
26
+ showUI?: boolean;
27
+ enableWindowDrop?: boolean;
15
28
  uiPlugins?: React.ReactNode[] | React.ReactNode;
16
29
  children?: React.ReactNode;
17
- } & import("react").RefAttributes<PrefabEditorRef>>;
30
+ }
31
+ declare const PrefabEditor: import("react").ForwardRefExoticComponent<PrefabEditorProps & import("react").RefAttributes<PrefabEditorRef>>;
18
32
  export default PrefabEditor;
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
11
  import GameCanvas from "../../shared/GameCanvas";
3
12
  import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
@@ -6,7 +15,7 @@ import { Physics } from "@react-three/rapier";
6
15
  import EditorUI from "./EditorUI";
7
16
  import { base, toolbar } from "./styles";
8
17
  import { EditorContext } from "./EditorContext";
9
- import { exportGLB, createModelNode, createImageNode } from "./utils";
18
+ import { createImageNode, createModelNode, exportGLB as exportSceneGLB, exportGLBData, insertNode } from "./utils";
10
19
  import { loadFiles } from "../dragdrop";
11
20
  const DEFAULT_PREFAB = {
12
21
  id: "prefab-default",
@@ -21,7 +30,7 @@ const DEFAULT_PREFAB = {
21
30
  }
22
31
  }
23
32
  };
24
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
33
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, showUI = true, enableWindowDrop = true, uiPlugins, children }, ref) => {
25
34
  const [editMode, setEditMode] = useState(true);
26
35
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
27
36
  const [selectedId, setSelectedId] = useState(null);
@@ -35,20 +44,71 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
35
44
  const lastDataRef = useRef(JSON.stringify(loadedPrefab));
36
45
  const prefabRootRef = useRef(null);
37
46
  const canvasRef = useRef(null);
47
+ const onPrefabChangeRef = useRef(onPrefabChange);
48
+ const pendingPrefabChangeRef = useRef(null);
49
+ useEffect(() => {
50
+ onPrefabChangeRef.current = onPrefabChange;
51
+ }, [onPrefabChange]);
52
+ const replacePrefab = (prefab, options) => {
53
+ if (throttleRef.current)
54
+ clearTimeout(throttleRef.current);
55
+ lastDataRef.current = JSON.stringify(prefab);
56
+ pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
57
+ setSelectedId(null);
58
+ setHistory([prefab]);
59
+ setHistoryIndex(0);
60
+ setLoadedPrefab(prefab);
61
+ };
38
62
  useEffect(() => {
39
63
  if (initialPrefab)
40
- setLoadedPrefab(initialPrefab);
64
+ replacePrefab(initialPrefab, { notifyChange: false });
41
65
  }, [initialPrefab]);
42
66
  const updatePrefab = (newPrefab) => {
43
- setLoadedPrefab(newPrefab);
44
- const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
45
- onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
67
+ setLoadedPrefab(prev => {
68
+ const resolved = typeof newPrefab === 'function' ? newPrefab(prev) : newPrefab;
69
+ if (Object.is(resolved, prev)) {
70
+ pendingPrefabChangeRef.current = null;
71
+ return prev;
72
+ }
73
+ pendingPrefabChangeRef.current = resolved;
74
+ return resolved;
75
+ });
76
+ };
77
+ useEffect(() => {
78
+ var _a;
79
+ if (pendingPrefabChangeRef.current !== loadedPrefab)
80
+ return;
81
+ (_a = onPrefabChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onPrefabChangeRef, loadedPrefab);
82
+ pendingPrefabChangeRef.current = null;
83
+ }, [loadedPrefab]);
84
+ const insertPrefabNode = (node, options) => {
85
+ updatePrefab(prev => {
86
+ return Object.assign(Object.assign({}, prev), { root: insertNode(prev.root, node, options === null || options === void 0 ? void 0 : options.parentId) });
87
+ });
88
+ if ((options === null || options === void 0 ? void 0 : options.select) !== false) {
89
+ setSelectedId(node.id);
90
+ }
91
+ return node;
92
+ };
93
+ const addModel = (path, model, options) => {
94
+ var _a;
95
+ const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
96
+ insertPrefabNode(node, options);
97
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(path, model);
98
+ return node;
99
+ };
100
+ const addTexture = (path, texture, options) => {
101
+ var _a;
102
+ const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
103
+ insertPrefabNode(node, options);
104
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(path, texture);
105
+ return node;
46
106
  };
47
107
  const applyHistory = (index) => {
48
108
  setHistoryIndex(index);
49
109
  lastDataRef.current = JSON.stringify(history[index]);
110
+ pendingPrefabChangeRef.current = history[index];
50
111
  setLoadedPrefab(history[index]);
51
- onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(history[index]);
52
112
  };
53
113
  const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
54
114
  const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
@@ -100,26 +160,28 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
100
160
  URL.revokeObjectURL(url);
101
161
  });
102
162
  };
103
- const handleExportGLB = () => {
163
+ const handleExportGLB = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
104
164
  var _a;
105
165
  const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
106
166
  if (!sceneRoot)
107
167
  return;
108
- exportGLB(sceneRoot, {
109
- filename: `${loadedPrefab.name || 'scene'}.glb`
110
- });
111
- };
168
+ return exportSceneGLB(sceneRoot, Object.assign({ filename: `${loadedPrefab.name || 'scene'}.glb` }, options));
169
+ });
170
+ const handleExportGLBData = () => __awaiter(void 0, void 0, void 0, function* () {
171
+ var _a;
172
+ const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
173
+ if (!sceneRoot)
174
+ return;
175
+ return exportGLBData(sceneRoot);
176
+ });
112
177
  const handleFocusNode = (nodeId) => {
113
178
  var _a;
114
179
  (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
115
180
  };
116
- useEffect(() => {
117
- const canvas = document.querySelector('canvas');
118
- if (canvas)
119
- canvasRef.current = canvas;
120
- }, []);
121
181
  // --- Drag & drop files to add nodes ---
122
182
  useEffect(() => {
183
+ if (!enableWindowDrop)
184
+ return;
123
185
  function handleDragOver(e) {
124
186
  e.preventDefault();
125
187
  e.stopPropagation();
@@ -131,26 +193,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
131
193
  const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
132
194
  void loadFiles(files, {
133
195
  onModelLoaded: (model, filename) => {
134
- var _a;
135
- const modelPath = `models/${filename}`;
136
- const baseName = filename.replace(/\.[^.]+$/, '');
137
- const newNode = createModelNode(modelPath, baseName);
138
- updatePrefab(prev => {
139
- var _a;
140
- return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
196
+ addModel(`models/${filename}`, model, {
197
+ name: filename.replace(/\.[^.]+$/, '')
141
198
  });
142
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
143
199
  },
144
200
  onTextureLoaded: (texture, filename) => {
145
- var _a;
146
- const texturePath = `textures/${filename}`;
147
- const baseName = filename.replace(/\.[^.]+$/, '');
148
- const newNode = createImageNode(texturePath, baseName);
149
- updatePrefab(prev => {
150
- var _a;
151
- return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
201
+ addTexture(`textures/${filename}`, texture, {
202
+ name: filename.replace(/\.[^.]+$/, '')
152
203
  });
153
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
154
204
  },
155
205
  onLoadError: error => {
156
206
  console.error('Drop asset error:', error);
@@ -163,12 +213,16 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
163
213
  window.removeEventListener('dragover', handleDragOver);
164
214
  window.removeEventListener('drop', handleDrop);
165
215
  };
166
- }, [loadedPrefab]);
216
+ }, [enableWindowDrop]);
167
217
  useImperativeHandle(ref, () => ({
168
218
  screenshot: handleScreenshot,
169
219
  exportGLB: handleExportGLB,
220
+ exportGLBData: handleExportGLBData,
170
221
  prefab: loadedPrefab,
171
- setPrefab: setLoadedPrefab,
222
+ setPrefab: replacePrefab,
223
+ replacePrefab,
224
+ addModel,
225
+ addTexture,
172
226
  rootRef: prefabRootRef
173
227
  }), [loadedPrefab]);
174
228
  const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, basePath: basePath }), children] }));
@@ -184,7 +238,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
184
238
  onFocusNode: handleFocusNode,
185
239
  onScreenshot: handleScreenshot,
186
240
  onExportGLB: handleExportGLB
187
- }, children: [_jsx(GameCanvas, { camera: { position: [0, 5, 15] }, children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), _jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
241
+ }, children: [_jsx(GameCanvas, { camera: { position: [0, 5, 15] }, canvasRef: canvasRef, children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] }))] });
188
242
  });
189
243
  PrefabEditor.displayName = "PrefabEditor";
190
244
  export default PrefabEditor;
@@ -36,6 +36,8 @@ export declare function findByComponent(root: GameObject, componentType: string)
36
36
  export declare function flatten(root: GameObject): GameObject[];
37
37
  /** Immutably update a node by ID */
38
38
  export declare function updateNode(root: GameObject, id: string, update: (node: GameObject) => GameObject): GameObject;
39
+ /** Immutably insert a node under a parent ID, defaulting to the root when the parent is missing */
40
+ export declare function insertNode(root: GameObject, node: GameObject, parentId?: string): GameObject;
39
41
  /** Immutably delete a node by ID */
40
42
  export declare function deleteNode(root: GameObject, id: string): GameObject | null;
41
43
  /** Deep clone a node with new IDs */
@@ -184,6 +184,21 @@ export function updateNode(root, id, update) {
184
184
  return root;
185
185
  return Object.assign(Object.assign({}, root), { children: root.children.map(child => updateNode(child, id, update)) });
186
186
  }
187
+ /** Immutably insert a node under a parent ID, defaulting to the root when the parent is missing */
188
+ export function insertNode(root, node, parentId) {
189
+ var _a, _b;
190
+ if (!parentId || parentId === root.id) {
191
+ return Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), node] });
192
+ }
193
+ const nextRoot = updateNode(root, parentId, parent => {
194
+ var _a;
195
+ return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), node] }));
196
+ });
197
+ if (nextRoot === root) {
198
+ return Object.assign(Object.assign({}, root), { children: [...((_b = root.children) !== null && _b !== void 0 ? _b : []), node] });
199
+ }
200
+ return nextRoot;
201
+ }
187
202
  /** Immutably delete a node by ID */
188
203
  export function deleteNode(root, id) {
189
204
  if (root.id === id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",