react-three-game 0.0.62 → 0.0.64

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 },
@@ -214,6 +216,7 @@ export function EmbeddedEditor({ prefab, onPrefabChange }: {
214
216
  showUI={false}
215
217
  physics={false}
216
218
  enableWindowDrop={false}
219
+ canvasProps={{ style: { height: '100%', width: '100%' } }}
217
220
  />
218
221
  </div>
219
222
  );
@@ -226,6 +229,7 @@ export function EmbeddedEditor({ prefab, onPrefabChange }: {
226
229
  - `addModel(path, model, options?)` creates a model node and injects the runtime asset in one step.
227
230
  - `addTexture(path, texture, options?)` creates a textured plane node and injects the runtime texture in one step.
228
231
  - `exportGLBData()` returns the GLB `ArrayBuffer` without triggering a download.
232
+ - `canvasProps` forwards canvas-level sizing, camera, event, and style props to `GameCanvas`.
229
233
  - `setPrefab(prefab)` remains as a backward-compatible alias for `replacePrefab(prefab)`.
230
234
 
231
235
  Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
@@ -6,5 +6,5 @@ interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
6
6
  glConfig?: WebGPURendererParameters;
7
7
  canvasRef?: React.RefObject<HTMLCanvasElement | null>;
8
8
  }
9
- export default function GameCanvas({ loader, children, glConfig, canvasRef, onCreated, ...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;
10
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, canvasRef, onCreated } = _a, props = __rest(_a, ["loader", "children", "glConfig", "canvasRef", "onCreated"]);
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));
@@ -1,3 +1,4 @@
1
+ import GameCanvas from "../../shared/GameCanvas";
1
2
  import { Object3D, Texture } from "three";
2
3
  import { GameObject, Prefab } from "./types";
3
4
  import { PrefabRootRef } from "./PrefabRoot";
@@ -11,6 +12,7 @@ export interface PrefabEditorRef {
11
12
  screenshot: () => void;
12
13
  exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | object | undefined>;
13
14
  exportGLBData: () => Promise<ArrayBuffer | undefined>;
15
+ clearSelection: () => Promise<void>;
14
16
  prefab: Prefab;
15
17
  setPrefab: (prefab: Prefab) => void;
16
18
  replacePrefab: (prefab: Prefab) => void;
@@ -25,6 +27,7 @@ export interface PrefabEditorProps {
25
27
  onPrefabChange?: (prefab: Prefab) => void;
26
28
  showUI?: boolean;
27
29
  enableWindowDrop?: boolean;
30
+ canvasProps?: Omit<React.ComponentProps<typeof GameCanvas>, 'children' | 'canvasRef'>;
28
31
  uiPlugins?: React.ReactNode[] | React.ReactNode;
29
32
  children?: React.ReactNode;
30
33
  }
@@ -30,7 +30,7 @@ const DEFAULT_PREFAB = {
30
30
  }
31
31
  }
32
32
  };
33
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, showUI = true, enableWindowDrop = true, uiPlugins, children }, ref) => {
33
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
34
34
  const [editMode, setEditMode] = useState(true);
35
35
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
36
36
  const [selectedId, setSelectedId] = useState(null);
@@ -46,6 +46,8 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
46
46
  const canvasRef = useRef(null);
47
47
  const onPrefabChangeRef = useRef(onPrefabChange);
48
48
  const pendingPrefabChangeRef = useRef(null);
49
+ const [injectedModels, setInjectedModels] = useState({});
50
+ const [injectedTextures, setInjectedTextures] = useState({});
49
51
  useEffect(() => {
50
52
  onPrefabChangeRef.current = onPrefabChange;
51
53
  }, [onPrefabChange]);
@@ -55,6 +57,8 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
55
57
  lastDataRef.current = JSON.stringify(prefab);
56
58
  pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
57
59
  setSelectedId(null);
60
+ setInjectedModels({});
61
+ setInjectedTextures({});
58
62
  setHistory([prefab]);
59
63
  setHistoryIndex(0);
60
64
  setLoadedPrefab(prefab);
@@ -91,17 +95,15 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
91
95
  return node;
92
96
  };
93
97
  const addModel = (path, model, options) => {
94
- var _a;
95
98
  const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
96
99
  insertPrefabNode(node, options);
97
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(path, model);
100
+ setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
98
101
  return node;
99
102
  };
100
103
  const addTexture = (path, texture, options) => {
101
- var _a;
102
104
  const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
103
105
  insertPrefabNode(node, options);
104
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(path, texture);
106
+ setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture })));
105
107
  return node;
106
108
  };
107
109
  const applyHistory = (index) => {
@@ -160,8 +162,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
160
162
  URL.revokeObjectURL(url);
161
163
  });
162
164
  };
165
+ const clearSelection = () => __awaiter(void 0, void 0, void 0, function* () {
166
+ if (!selectedId)
167
+ return;
168
+ setSelectedId(null);
169
+ yield new Promise(resolve => {
170
+ requestAnimationFrame(() => {
171
+ requestAnimationFrame(() => resolve());
172
+ });
173
+ });
174
+ });
163
175
  const handleExportGLB = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
164
176
  var _a;
177
+ yield clearSelection();
165
178
  const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
166
179
  if (!sceneRoot)
167
180
  return;
@@ -169,6 +182,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
169
182
  });
170
183
  const handleExportGLBData = () => __awaiter(void 0, void 0, void 0, function* () {
171
184
  var _a;
185
+ yield clearSelection();
172
186
  const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
173
187
  if (!sceneRoot)
174
188
  return;
@@ -218,6 +232,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
218
232
  screenshot: handleScreenshot,
219
233
  exportGLB: handleExportGLB,
220
234
  exportGLBData: handleExportGLBData,
235
+ clearSelection,
221
236
  prefab: loadedPrefab,
222
237
  setPrefab: replacePrefab,
223
238
  replacePrefab,
@@ -225,7 +240,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
225
240
  addTexture,
226
241
  rootRef: prefabRootRef
227
242
  }), [loadedPrefab]);
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] }));
243
+ 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] }));
229
244
  return _jsxs(EditorContext.Provider, { value: {
230
245
  transformMode,
231
246
  setTransformMode,
@@ -238,7 +253,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
238
253
  onFocusNode: handleFocusNode,
239
254
  onScreenshot: handleScreenshot,
240
255
  onExportGLB: handleExportGLB
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 })] }))] });
256
+ }, 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 })] }))] });
242
257
  });
243
258
  PrefabEditor.displayName = "PrefabEditor";
244
259
  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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",