react-three-game 0.0.61 → 0.0.63

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
@@ -3,7 +3,7 @@
3
3
  JSON-first 3D game engine. React Three Fiber + WebGPU + Rapier Physics.
4
4
 
5
5
  ```bash
6
- npm i react-three-game @react-three/fiber @react-three/rapier three
6
+ npm i react-three-game @react-three/drei @react-three/fiber @react-three/rapier three
7
7
  ```
8
8
 
9
9
  ![Prefab Editor](assets/editor.gif)
@@ -102,8 +102,10 @@ interface GameObject {
102
102
  ## Custom Components
103
103
 
104
104
  ```tsx
105
+ import { useRef } from 'react';
105
106
  import { Component, registerComponent, FieldRenderer, FieldDefinition } from 'react-three-game';
106
107
  import { useFrame } from '@react-three/fiber';
108
+ import type { Group } from 'three';
107
109
 
108
110
  const rotatorFields: FieldDefinition[] = [
109
111
  { name: 'speed', type: 'number', label: 'Speed', step: 0.1 },
@@ -161,17 +163,75 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
161
163
  ## Prefab Editor
162
164
 
163
165
  ```jsx
166
+ import { useRef } from 'react';
164
167
  import { PrefabEditor } from 'react-three-game';
165
168
 
166
169
  // Standalone editor
167
170
  <PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
168
171
 
172
+ // Canvas-only editing mode (keeps canvas selection/gizmos, hides hierarchy + inspector + toolbar)
173
+ <PrefabEditor initialPrefab={sceneData} showUI={false} />
174
+
169
175
  // With custom R3F components
170
176
  <PrefabEditor initialPrefab={sceneData}>
171
177
  <CustomComponent />
172
178
  </PrefabEditor>
173
179
  ```
174
180
 
181
+ ### Embedded / Headless Editor
182
+
183
+ ```tsx
184
+ import { useRef } from 'react';
185
+ import type { Object3D } from 'three';
186
+ import { PrefabEditor, type PrefabEditorRef } from 'react-three-game';
187
+
188
+ export function EmbeddedEditor({ prefab, onPrefabChange }: {
189
+ prefab: any;
190
+ onPrefabChange: (nextPrefab: any) => void;
191
+ }) {
192
+ const editorRef = useRef<PrefabEditorRef>(null);
193
+
194
+ function loadScene(nextPrefab: any) {
195
+ editorRef.current?.replacePrefab(nextPrefab);
196
+ }
197
+
198
+ function importRuntimeModel(model: Object3D) {
199
+ editorRef.current?.addModel('models/runtime/chair.glb', model, {
200
+ name: 'Chair',
201
+ parentId: 'root',
202
+ });
203
+ }
204
+
205
+ return (
206
+ <div style={{ position: 'relative', height: 600 }}>
207
+ <div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
208
+ <button onClick={() => loadScene(prefab)}>Reload Scene</button>
209
+ <button onClick={() => editorRef.current?.exportGLBData()}>Export GLB Data</button>
210
+ </div>
211
+
212
+ <PrefabEditor
213
+ ref={editorRef}
214
+ initialPrefab={prefab}
215
+ onPrefabChange={onPrefabChange}
216
+ showUI={false}
217
+ physics={false}
218
+ enableWindowDrop={false}
219
+ canvasProps={{ style: { height: '100%', width: '100%' } }}
220
+ />
221
+ </div>
222
+ );
223
+ }
224
+ ```
225
+
226
+ `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`:
227
+
228
+ - `replacePrefab(prefab)` replaces the current scene through the editor state pipeline and resets editor history/selection.
229
+ - `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
230
+ - `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
231
+ - `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
232
+ - `canvasProps` forwards canvas-level sizing, camera, event, and style props to `GameCanvas`.
233
+ - `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
234
+
175
235
  Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
176
236
 
177
237
  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, style, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
9
10
  export {};
@@ -31,9 +31,9 @@ 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, style } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated", "style"]);
35
35
  const [frameloop, setFrameloop] = useState("never");
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 }) {
36
+ return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: Object.assign({ touchAction: 'none', userSelect: 'none' }, style), shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
37
37
  const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
38
38
  // @ts-expect-error futuristic
39
39
  shadowMap: true, antialias: true }, glConfig));
@@ -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,34 @@
1
- import { Prefab } from "./types";
1
+ import GameCanvas from "../../shared/GameCanvas";
2
+ import { Object3D, Texture } from "three";
3
+ import { GameObject, Prefab } from "./types";
2
4
  import { PrefabRootRef } from "./PrefabRoot";
5
+ import type { ExportGLBOptions } from "./utils";
6
+ export interface PrefabEditorAssetOptions {
7
+ name?: string;
8
+ parentId?: string;
9
+ select?: boolean;
10
+ }
3
11
  export interface PrefabEditorRef {
4
12
  screenshot: () => void;
5
- exportGLB: () => void;
13
+ exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | object | undefined>;
14
+ exportGLBData: () => Promise<ArrayBuffer | undefined>;
6
15
  prefab: Prefab;
7
16
  setPrefab: (prefab: Prefab) => void;
17
+ replacePrefab: (prefab: Prefab) => void;
18
+ addModel: (path: string, model: Object3D, options?: PrefabEditorAssetOptions) => GameObject;
19
+ addTexture: (path: string, texture: Texture, options?: PrefabEditorAssetOptions) => GameObject;
8
20
  rootRef: React.RefObject<PrefabRootRef | null>;
9
21
  }
10
- declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
22
+ export interface PrefabEditorProps {
11
23
  basePath?: string;
12
24
  initialPrefab?: Prefab;
13
25
  physics?: boolean;
14
26
  onPrefabChange?: (prefab: Prefab) => void;
27
+ showUI?: boolean;
28
+ enableWindowDrop?: boolean;
29
+ canvasProps?: Omit<React.ComponentProps<typeof GameCanvas>, 'children' | 'canvasRef'>;
15
30
  uiPlugins?: React.ReactNode[] | React.ReactNode;
16
31
  children?: React.ReactNode;
17
- } & import("react").RefAttributes<PrefabEditorRef>>;
32
+ }
33
+ declare const PrefabEditor: import("react").ForwardRefExoticComponent<PrefabEditorProps & import("react").RefAttributes<PrefabEditorRef>>;
18
34
  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, canvasProps, 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,73 @@ 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
+ const [injectedModels, setInjectedModels] = useState({});
50
+ const [injectedTextures, setInjectedTextures] = useState({});
51
+ useEffect(() => {
52
+ onPrefabChangeRef.current = onPrefabChange;
53
+ }, [onPrefabChange]);
54
+ const replacePrefab = (prefab, options) => {
55
+ if (throttleRef.current)
56
+ clearTimeout(throttleRef.current);
57
+ lastDataRef.current = JSON.stringify(prefab);
58
+ pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
59
+ setSelectedId(null);
60
+ setInjectedModels({});
61
+ setInjectedTextures({});
62
+ setHistory([prefab]);
63
+ setHistoryIndex(0);
64
+ setLoadedPrefab(prefab);
65
+ };
38
66
  useEffect(() => {
39
67
  if (initialPrefab)
40
- setLoadedPrefab(initialPrefab);
68
+ replacePrefab(initialPrefab, { notifyChange: false });
41
69
  }, [initialPrefab]);
42
70
  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);
71
+ setLoadedPrefab(prev => {
72
+ const resolved = typeof newPrefab === 'function' ? newPrefab(prev) : newPrefab;
73
+ if (Object.is(resolved, prev)) {
74
+ pendingPrefabChangeRef.current = null;
75
+ return prev;
76
+ }
77
+ pendingPrefabChangeRef.current = resolved;
78
+ return resolved;
79
+ });
80
+ };
81
+ useEffect(() => {
82
+ var _a;
83
+ if (pendingPrefabChangeRef.current !== loadedPrefab)
84
+ return;
85
+ (_a = onPrefabChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onPrefabChangeRef, loadedPrefab);
86
+ pendingPrefabChangeRef.current = null;
87
+ }, [loadedPrefab]);
88
+ const insertPrefabNode = (node, options) => {
89
+ updatePrefab(prev => {
90
+ return Object.assign(Object.assign({}, prev), { root: insertNode(prev.root, node, options === null || options === void 0 ? void 0 : options.parentId) });
91
+ });
92
+ if ((options === null || options === void 0 ? void 0 : options.select) !== false) {
93
+ setSelectedId(node.id);
94
+ }
95
+ return node;
96
+ };
97
+ const addModel = (path, model, options) => {
98
+ const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
99
+ insertPrefabNode(node, options);
100
+ setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
101
+ return node;
102
+ };
103
+ const addTexture = (path, texture, options) => {
104
+ const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
105
+ insertPrefabNode(node, options);
106
+ setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture })));
107
+ return node;
46
108
  };
47
109
  const applyHistory = (index) => {
48
110
  setHistoryIndex(index);
49
111
  lastDataRef.current = JSON.stringify(history[index]);
112
+ pendingPrefabChangeRef.current = history[index];
50
113
  setLoadedPrefab(history[index]);
51
- onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(history[index]);
52
114
  };
53
115
  const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
54
116
  const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
@@ -100,26 +162,28 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
100
162
  URL.revokeObjectURL(url);
101
163
  });
102
164
  };
103
- const handleExportGLB = () => {
165
+ const handleExportGLB = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
104
166
  var _a;
105
167
  const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
106
168
  if (!sceneRoot)
107
169
  return;
108
- exportGLB(sceneRoot, {
109
- filename: `${loadedPrefab.name || 'scene'}.glb`
110
- });
111
- };
170
+ return exportSceneGLB(sceneRoot, Object.assign({ filename: `${loadedPrefab.name || 'scene'}.glb` }, options));
171
+ });
172
+ const handleExportGLBData = () => __awaiter(void 0, void 0, void 0, function* () {
173
+ var _a;
174
+ const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
175
+ if (!sceneRoot)
176
+ return;
177
+ return exportGLBData(sceneRoot);
178
+ });
112
179
  const handleFocusNode = (nodeId) => {
113
180
  var _a;
114
181
  (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
115
182
  };
116
- useEffect(() => {
117
- const canvas = document.querySelector('canvas');
118
- if (canvas)
119
- canvasRef.current = canvas;
120
- }, []);
121
183
  // --- Drag & drop files to add nodes ---
122
184
  useEffect(() => {
185
+ if (!enableWindowDrop)
186
+ return;
123
187
  function handleDragOver(e) {
124
188
  e.preventDefault();
125
189
  e.stopPropagation();
@@ -131,26 +195,14 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
131
195
  const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
132
196
  void loadFiles(files, {
133
197
  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] }) }));
198
+ addModel(`models/${filename}`, model, {
199
+ name: filename.replace(/\.[^.]+$/, '')
141
200
  });
142
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
143
201
  },
144
202
  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] }) }));
203
+ addTexture(`textures/${filename}`, texture, {
204
+ name: filename.replace(/\.[^.]+$/, '')
152
205
  });
153
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
154
206
  },
155
207
  onLoadError: error => {
156
208
  console.error('Drop asset error:', error);
@@ -163,15 +215,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
163
215
  window.removeEventListener('dragover', handleDragOver);
164
216
  window.removeEventListener('drop', handleDrop);
165
217
  };
166
- }, [loadedPrefab]);
218
+ }, [enableWindowDrop]);
167
219
  useImperativeHandle(ref, () => ({
168
220
  screenshot: handleScreenshot,
169
221
  exportGLB: handleExportGLB,
222
+ exportGLBData: handleExportGLBData,
170
223
  prefab: loadedPrefab,
171
- setPrefab: setLoadedPrefab,
224
+ setPrefab: replacePrefab,
225
+ replacePrefab,
226
+ addModel,
227
+ addTexture,
172
228
  rootRef: prefabRootRef
173
229
  }), [loadedPrefab]);
174
- 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] }));
230
+ 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, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
175
231
  return _jsxs(EditorContext.Provider, { value: {
176
232
  transformMode,
177
233
  setTransformMode,
@@ -184,7 +240,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
184
240
  onFocusNode: handleFocusNode,
185
241
  onScreenshot: handleScreenshot,
186
242
  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 })] });
243
+ }, children: [_jsx(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { 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
244
  });
189
245
  PrefabEditor.displayName = "PrefabEditor";
190
246
  export default PrefabEditor;
@@ -4,8 +4,6 @@ import { Prefab, GameObject as GameObjectType } from "./types";
4
4
  export interface PrefabRootRef {
5
5
  root: Group | null;
6
6
  rigidBodyRefs: Map<string, any>;
7
- injectModel: (filename: string, model: Object3D) => void;
8
- injectTexture: (filename: string, texture: Texture) => void;
9
7
  focusNode: (nodeId: string) => void;
10
8
  }
11
9
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
@@ -16,6 +14,8 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
16
14
  onSelect?: (id: string | null) => void;
17
15
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
18
16
  basePath?: string;
17
+ injectedModels?: Record<string, Object3D>;
18
+ injectedTextures?: Record<string, Texture>;
19
19
  } & import("react").RefAttributes<PrefabRootRef>>;
20
20
  export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
21
21
  interface RendererProps {
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { MapControls, TransformControls, useHelper } from "@react-three/drei";
12
- import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
12
+ import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
13
13
  import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
14
14
  import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
15
15
  import components from "./components";
@@ -19,7 +19,7 @@ import { focusCameraOnObject, updateNode } from "./utils";
19
19
  import { EditorContext } from "./EditorContext";
20
20
  components.forEach(registerComponent);
21
21
  const IDENTITY = new Matrix4();
22
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "" }, ref) => {
22
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "", injectedModels = {}, injectedTextures = {} }, ref) => {
23
23
  var _a, _b, _c, _d;
24
24
  // optional editor context
25
25
  const editorContext = useContext(EditorContext);
@@ -37,18 +37,11 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
37
37
  const [selectedObject, setSelectedObject] = useState(null);
38
38
  const rootRef = useRef(null);
39
39
  const controlsRef = useRef(null);
40
- const injectModel = useCallback((filename, model) => {
41
- setModels(m => (Object.assign(Object.assign({}, m), { [filename]: model })));
42
- }, []);
43
- const injectTexture = useCallback((filename, texture) => {
44
- loading.current.add(filename);
45
- setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: texture })));
46
- }, []);
40
+ const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
41
+ const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
47
42
  useImperativeHandle(ref, () => ({
48
43
  root: rootRef.current,
49
44
  rigidBodyRefs: rigidBodyRefs.current,
50
- injectModel,
51
- injectTexture,
52
45
  focusNode: (nodeId) => {
53
46
  const object = objectRefs.current[nodeId];
54
47
  const controls = controlsRef.current;
@@ -57,7 +50,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
57
50
  return;
58
51
  focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
59
52
  }
60
- }), [injectModel, injectTexture]);
53
+ }), []);
61
54
  const registerRef = useCallback((id, obj) => {
62
55
  objectRefs.current[id] = obj;
63
56
  if (id === selectedId)
@@ -107,7 +100,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
107
100
  texturesToLoad.add(node.components.material.properties.normalMapTexture);
108
101
  });
109
102
  modelsToLoad.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
110
- if (models[file] || loading.current.has(file))
103
+ if (availableModels[file] || loading.current.has(file))
111
104
  return;
112
105
  loading.current.add(file);
113
106
  const path = file.startsWith("/")
@@ -121,7 +114,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
121
114
  }));
122
115
  const loader = new TextureLoader();
123
116
  texturesToLoad.forEach(file => {
124
- if (textures[file] || loading.current.has(file) || failedTextures.current.has(file))
117
+ if (availableTextures[file] || loading.current.has(file) || failedTextures.current.has(file))
125
118
  return;
126
119
  loading.current.add(file);
127
120
  // Handle full URLs (http/https) or regular paths
@@ -139,8 +132,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
139
132
  failedTextures.current.add(file);
140
133
  });
141
134
  });
142
- }, [data, models, textures]);
143
- return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`))] }))] }));
135
+ }, [data, availableModels, availableTextures, basePath]);
136
+ return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: availableModels, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`))] }))] }));
144
137
  });
145
138
  export function GameObjectRenderer(props) {
146
139
  var _a, _b, _c;
@@ -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.63",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",