react-three-game 0.0.36 → 0.0.38

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.
Files changed (34) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.js +5 -5
  3. package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
  4. package/dist/tools/prefabeditor/EditorContext.js +9 -0
  5. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
  6. package/dist/tools/prefabeditor/EditorTree.js +38 -3
  7. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
  8. package/dist/tools/prefabeditor/EditorUI.js +4 -2
  9. package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
  10. package/dist/tools/prefabeditor/ExportHelper.js +55 -0
  11. package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -0
  12. package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
  14. package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +7 -2
  16. package/dist/tools/prefabeditor/PrefabRoot.js +78 -49
  17. package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
  18. package/dist/tools/prefabeditor/components/Input.js +9 -3
  19. package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
  20. package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
  21. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  22. package/dist/tools/prefabeditor/utils.js +41 -0
  23. package/package.json +1 -1
  24. package/src/index.ts +12 -12
  25. package/src/tools/prefabeditor/EditorContext.tsx +20 -0
  26. package/src/tools/prefabeditor/EditorTree.tsx +83 -22
  27. package/src/tools/prefabeditor/EditorUI.tsx +2 -10
  28. package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
  29. package/src/tools/prefabeditor/PrefabEditor.tsx +80 -51
  30. package/src/tools/prefabeditor/PrefabRoot.tsx +181 -69
  31. package/src/tools/prefabeditor/components/Input.tsx +11 -3
  32. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
  33. package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
  34. package/src/tools/prefabeditor/utils.ts +43 -1
@@ -10,50 +10,50 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { MapControls, TransformControls, useHelper } from "@react-three/drei";
13
- import { forwardRef, useCallback, useEffect, useRef, useState, } from "react";
13
+ import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
14
14
  import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
15
15
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
16
16
  import components from "./components";
17
17
  import { loadModel } from "../dragdrop/modelLoader";
18
- import { GameInstance, GameInstanceProvider } from "./InstanceProvider";
18
+ import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
19
19
  import { updateNode } from "./utils";
20
- /* -------------------------------------------------- */
21
- /* Setup */
22
- /* -------------------------------------------------- */
20
+ import { EditorContext } from "./EditorContext";
23
21
  components.forEach(registerComponent);
24
22
  const IDENTITY = new Matrix4();
25
- /* -------------------------------------------------- */
26
- /* PrefabRoot */
27
- /* -------------------------------------------------- */
28
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, basePath = "" }, ref) => {
23
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "" }, ref) => {
24
+ var _a, _b;
25
+ // optional editor context
26
+ const editorContext = useContext(EditorContext);
27
+ const transformMode = (_a = editorContext === null || editorContext === void 0 ? void 0 : editorContext.transformMode) !== null && _a !== void 0 ? _a : "translate";
28
+ const snapResolution = (_b = editorContext === null || editorContext === void 0 ? void 0 : editorContext.snapResolution) !== null && _b !== void 0 ? _b : 0;
29
+ // prefab root state
29
30
  const [models, setModels] = useState({});
30
31
  const [textures, setTextures] = useState({});
31
32
  const loading = useRef(new Set());
32
33
  const objectRefs = useRef({});
33
34
  const [selectedObject, setSelectedObject] = useState(null);
35
+ const rootRef = useRef(null);
36
+ useImperativeHandle(ref, () => ({
37
+ root: rootRef.current
38
+ }), []);
34
39
  const registerRef = useCallback((id, obj) => {
35
40
  objectRefs.current[id] = obj;
36
41
  if (id === selectedId)
37
42
  setSelectedObject(obj);
38
43
  }, [selectedId]);
39
- // Suppress TransformControls scene graph warnings during transitions
40
44
  useEffect(() => {
41
45
  const originalError = console.error;
42
46
  console.error = (...args) => {
43
- if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph')) {
44
- return; // Suppress this specific error
45
- }
47
+ if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph'))
48
+ return;
46
49
  originalError.apply(console, args);
47
50
  };
48
- return () => {
49
- console.error = originalError;
50
- };
51
+ return () => { console.error = originalError; };
51
52
  }, []);
52
53
  useEffect(() => {
53
54
  var _a;
54
55
  setSelectedObject(selectedId ? (_a = objectRefs.current[selectedId]) !== null && _a !== void 0 ? _a : null : null);
55
56
  }, [selectedId]);
56
- /* ---------------- Transform writeback ---------------- */
57
57
  const onTransformChange = () => {
58
58
  if (!selectedId || !onPrefabChange)
59
59
  return;
@@ -69,7 +69,6 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
69
69
  } }) })));
70
70
  onPrefabChange(Object.assign(Object.assign({}, data), { root }));
71
71
  };
72
- /* ---------------- Asset loading ---------------- */
73
72
  useEffect(() => {
74
73
  const modelsToLoad = new Set();
75
74
  const texturesToLoad = new Set();
@@ -105,45 +104,70 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
105
104
  });
106
105
  });
107
106
  }, [data, models, textures]);
108
- /* ---------------- Render ---------------- */
109
- return (_jsxs("group", { ref: ref, 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, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] }))] }));
107
+ 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, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: snapResolution > 0 ? snapResolution : undefined, rotationSnap: snapResolution > 0 ? snapResolution : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${snapResolution}`))] }))] }));
110
108
  });
111
- /* -------------------------------------------------- */
112
- /* Renderer Switch */
113
- /* -------------------------------------------------- */
114
109
  export function GameObjectRenderer(props) {
115
110
  var _a, _b, _c;
116
111
  const node = props.gameObject;
117
112
  if (!node || node.hidden || node.disabled)
118
113
  return null;
119
- return ((_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced)
120
- ? _jsx(InstancedNode, Object.assign({}, props))
121
- : _jsx(StandardNode, Object.assign({}, props));
114
+ const isInstanced = (_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
115
+ const prevInstancedRef = useRef(undefined);
116
+ const [isTransitioning, setIsTransitioning] = useState(false);
117
+ useEffect(() => {
118
+ if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
119
+ setIsTransitioning(true);
120
+ const timer = setTimeout(() => setIsTransitioning(false), 100);
121
+ return () => clearTimeout(timer);
122
+ }
123
+ prevInstancedRef.current = isInstanced;
124
+ }, [isInstanced]);
125
+ if (isTransitioning)
126
+ return null;
127
+ const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
128
+ return isInstanced
129
+ ? _jsx(InstancedNode, Object.assign({}, props), key)
130
+ : _jsx(StandardNode, Object.assign({}, props), key);
122
131
  }
123
- /* -------------------------------------------------- */
124
- /* InstancedNode (terminal) */
125
- /* -------------------------------------------------- */
126
132
  function isPhysicsProps(v) {
127
133
  return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic";
128
134
  }
129
- function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode }) {
130
- var _a, _b, _c, _d, _e, _f, _g;
135
+ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, registerRef, selectedId: _selectedId, onSelect, onClick }) {
136
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
131
137
  const world = parentMatrix.clone().multiply(compose(gameObject));
132
- const { position, rotation, scale } = decompose(world);
138
+ const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
139
+ const localTransform = getNodeTransformProps(gameObject);
133
140
  const physicsProps = isPhysicsProps((_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics) === null || _b === void 0 ? void 0 : _b.properties)
134
141
  ? (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.physics) === null || _d === void 0 ? void 0 : _d.properties
135
142
  : undefined;
136
- return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename, position: position, rotation: rotation, scale: scale, physics: editMode ? undefined : physicsProps }));
143
+ const groupRef = useRef(null);
144
+ const clickValid = useRef(false);
145
+ useEffect(() => {
146
+ if (editMode) {
147
+ registerRef(gameObject.id, groupRef.current);
148
+ return () => registerRef(gameObject.id, null);
149
+ }
150
+ }, [gameObject.id, registerRef, editMode]);
151
+ const modelUrl = (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename;
152
+ if (editMode) {
153
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
154
+ if (clickValid.current) {
155
+ e.stopPropagation();
156
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
157
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
158
+ }
159
+ clickValid.current = false;
160
+ }, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx(GameInstance, { id: gameObject.id, modelUrl: modelUrl, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps })] }));
161
+ }
162
+ return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_k = (_j = (_h = gameObject.components) === null || _h === void 0 ? void 0 : _h.model) === null || _j === void 0 ? void 0 : _j.properties) === null || _k === void 0 ? void 0 : _k.filename, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps }));
137
163
  }
138
- /* -------------------------------------------------- */
139
- /* StandardNode */
140
- /* -------------------------------------------------- */
141
- function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
142
- var _a, _b, _c;
164
+ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
165
+ var _a, _b, _c, _d, _e, _f;
143
166
  const groupRef = useRef(null);
167
+ const helperRef = useRef(null);
144
168
  const clickValid = useRef(false);
145
169
  const isSelected = selectedId === gameObject.id;
146
- const helperRef = groupRef;
170
+ const stillInstanced = useInstanceCheck(gameObject.id);
147
171
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
148
172
  useEffect(() => {
149
173
  registerRef(gameObject.id, groupRef.current);
@@ -158,20 +182,26 @@ function StandardNode({ gameObject, selectedId, onSelect, registerRef, loadedMod
158
182
  if (clickValid.current) {
159
183
  e.stopPropagation();
160
184
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(gameObject.id);
185
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
161
186
  }
162
187
  clickValid.current = false;
163
188
  };
164
- const inner = (_jsxs("group", Object.assign({ ref: groupRef }, getNodeTransformProps(gameObject), { onPointerDown: onDown, onPointerMove: () => (clickValid.current = false), onPointerUp: onUp, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_a = gameObject.children) === null || _a === void 0 ? void 0 : _a.map(child => (_jsx(GameObjectRenderer, { child, gameObject: child, selectedId: selectedId, onSelect: onSelect, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] })));
165
- const physics = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.physics;
166
- const ready = !((_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) ||
189
+ const physics = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics;
190
+ const ready = !((_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) ||
167
191
  loadedModels[gameObject.components.model.properties.filename];
168
- if (physics && !editMode && ready) {
169
- const def = getComponent("Physics");
170
- return (def === null || def === void 0 ? void 0 : def.View)
171
- ? _jsx(def.View, { properties: physics.properties, children: inner })
172
- : inner;
192
+ const hasPhysics = physics && ready && !stillInstanced;
193
+ const transform = getNodeTransformProps(gameObject);
194
+ const physicsDef = hasPhysics ? getComponent("Physics") : null;
195
+ const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
196
+ const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
197
+ const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
198
+ if (editMode) {
199
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey)) : null] }));
200
+ }
201
+ if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
202
+ return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey));
173
203
  }
174
- return inner;
204
+ return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, onPointerDown: onDown, onPointerMove: () => (clickValid.current = false), onPointerUp: onUp, children: inner }));
175
205
  }
176
206
  function walk(node, fn) {
177
207
  var _a;
@@ -241,7 +271,6 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
241
271
  const def = getComponent(comp.type);
242
272
  if (!(def === null || def === void 0 ? void 0 : def.View))
243
273
  return;
244
- // crude but works with your existing component API
245
274
  if (def.View.toString().includes("children")) {
246
275
  wrappers.push({ key, View: def.View, properties: comp.properties });
247
276
  }
@@ -11,9 +11,10 @@ export declare function Input({ value, onChange, step, min, max, style }: InputP
11
11
  export declare function Label({ children }: {
12
12
  children: React.ReactNode;
13
13
  }): import("react/jsx-runtime").JSX.Element;
14
- export declare function Vector3Input({ label, value, onChange }: {
14
+ export declare function Vector3Input({ label, value, onChange, snap }: {
15
15
  label: string;
16
16
  value: [number, number, number];
17
17
  onChange: (v: [number, number, number]) => void;
18
+ snap?: number;
18
19
  }): import("react/jsx-runtime").JSX.Element;
19
20
  export {};
@@ -27,7 +27,12 @@ export function Input({ value, onChange, step, min, max, style }) {
27
27
  export function Label({ children }) {
28
28
  return _jsx("label", { style: styles.label, children: children });
29
29
  }
30
- export function Vector3Input({ label, value, onChange }) {
30
+ export function Vector3Input({ label, value, onChange, snap }) {
31
+ const snapValue = (num) => {
32
+ if (!snap)
33
+ return num;
34
+ return Math.round(num / snap) * snap;
35
+ };
31
36
  const [draft, setDraft] = useState(() => value.map(v => v.toString()));
32
37
  // Sync external changes (gizmo, undo, etc.)
33
38
  useEffect(() => {
@@ -38,7 +43,7 @@ export function Vector3Input({ label, value, onChange }) {
38
43
  const num = parseFloat(draft[index]);
39
44
  if (Number.isFinite(num)) {
40
45
  const next = [...value];
41
- next[index] = num;
46
+ next[index] = snapValue(num);
42
47
  onChange(next);
43
48
  }
44
49
  };
@@ -62,7 +67,8 @@ export function Vector3Input({ label, value, onChange }) {
62
67
  speed *= 0.1; // fine
63
68
  if (e.altKey)
64
69
  speed *= 5; // coarse
65
- const nextValue = startValue + dx * speed;
70
+ const rawValue = startValue + dx * speed;
71
+ const nextValue = snapValue(rawValue);
66
72
  const next = [...value];
67
73
  next[index] = nextValue;
68
74
  setDraft(d => {
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { RigidBody } from "@react-three/rapier";
3
3
  import { Label } from "./Input";
4
4
  function PhysicsComponentEditor({ component, onUpdate }) {
@@ -15,13 +15,14 @@ function PhysicsComponentEditor({ component, onUpdate }) {
15
15
  };
16
16
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Type" }), _jsxs("select", { style: selectStyle, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Collider" }), _jsxs("select", { style: selectStyle, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] })] }));
17
17
  }
18
- function PhysicsComponentView({ properties, editMode, children }) {
19
- if (editMode)
20
- return _jsx(_Fragment, { children: children });
18
+ function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }) {
21
19
  const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
22
- // Remount RigidBody when collider/type changes to avoid Rapier hook dependency warnings
23
- const rbKey = `${properties.type || 'dynamic'}_${colliders}`;
24
- return (_jsx(RigidBody, { type: properties.type, colliders: colliders, children: children }, rbKey));
20
+ // In edit mode, include position/rotation in key to force remount when transform changes
21
+ // This ensures the RigidBody debug visualization updates even when physics is paused
22
+ const rbKey = editMode
23
+ ? `${properties.type || 'dynamic'}_${colliders}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
24
+ : `${properties.type || 'dynamic'}_${colliders}`;
25
+ return (_jsx(RigidBody, { type: properties.type, colliders: colliders, position: position, rotation: rotation, scale: scale, children: children }, rbKey));
25
26
  }
26
27
  const PhysicsComponent = {
27
28
  name: 'Physics',
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Vector3Input, Label } from "./Input";
3
+ import { useEditorContext } from "../EditorContext";
3
4
  const buttonStyle = {
4
5
  padding: '2px 6px',
5
6
  background: 'transparent',
@@ -11,7 +12,8 @@ const buttonStyle = {
11
12
  flex: 1,
12
13
  };
13
14
  function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
14
- return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx(Label, { children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
15
+ const { snapResolution, setSnapResolution } = useEditorContext();
16
+ return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs(Label, { children: ["Transform Mode ", snapResolution > 0 && `(Snap: ${snapResolution})`] }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => {
15
17
  const isActive = transformMode === mode;
16
18
  return (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign({}, buttonStyle), { background: isActive ? 'rgba(255,255,255,0.10)' : 'transparent' }), onPointerEnter: (e) => {
17
19
  if (!isActive)
@@ -20,7 +22,13 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
20
22
  if (!isActive)
21
23
  e.currentTarget.style.background = 'transparent';
22
24
  }, children: mode }, mode));
23
- }) })] })), _jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }) }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }) }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }) })] });
25
+ }) }), _jsx("div", { style: { marginTop: 6 }, children: _jsxs("button", { onClick: () => setSnapResolution(snapResolution > 0 ? 0 : 0.1), style: Object.assign(Object.assign({}, buttonStyle), { background: snapResolution > 0 ? 'rgba(255,255,255,0.10)' : 'transparent', width: '100%' }), onPointerEnter: (e) => {
26
+ if (snapResolution === 0)
27
+ e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
28
+ }, onPointerLeave: (e) => {
29
+ if (snapResolution === 0)
30
+ e.currentTarget.style.background = 'transparent';
31
+ }, children: ["Snap: ", snapResolution > 0 ? `ON (${snapResolution})` : 'OFF'] }) })] })), _jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }), snap: snapResolution }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }), snap: snapResolution }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }), snap: snapResolution })] });
24
32
  }
25
33
  const TransformComponent = {
26
34
  name: 'Transform',
@@ -1,4 +1,8 @@
1
- import { GameObject } from "./types";
1
+ import { GameObject, Prefab } from "./types";
2
+ /** Save a prefab as JSON file */
3
+ export declare function saveJson(data: Prefab, filename: string): void;
4
+ /** Load a prefab from JSON file */
5
+ export declare function loadJson(): Promise<Prefab | undefined>;
2
6
  /** Find a node by ID in the tree */
3
7
  export declare function findNode(root: GameObject, id: string): GameObject | null;
4
8
  /** Find the parent of a node by ID */
@@ -15,6 +19,8 @@ export declare function updateNode(root: GameObject, id: string, update: (node:
15
19
  export declare function deleteNode(root: GameObject, id: string): GameObject | null;
16
20
  /** Deep clone a node with new IDs */
17
21
  export declare function cloneNode(node: GameObject): GameObject;
22
+ /** Recursively update all IDs in a node tree */
23
+ export declare function regenerateIds(node: GameObject): GameObject;
18
24
  /** Get component data from a node */
19
25
  export declare function getComponent<T = any>(node: GameObject, type: string): T | undefined;
20
26
  export declare function updateNodeById(root: GameObject, id: string, updater: (node: GameObject) => GameObject): GameObject;
@@ -1,3 +1,39 @@
1
+ /** Save a prefab as JSON file */
2
+ export function saveJson(data, filename) {
3
+ const a = document.createElement('a');
4
+ a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
5
+ a.download = `${filename || 'prefab'}.json`;
6
+ a.click();
7
+ }
8
+ /** Load a prefab from JSON file */
9
+ export function loadJson() {
10
+ return new Promise(resolve => {
11
+ const input = document.createElement('input');
12
+ input.type = 'file';
13
+ input.accept = '.json,application/json';
14
+ input.onchange = e => {
15
+ var _a;
16
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
17
+ if (!file)
18
+ return resolve(undefined);
19
+ const reader = new FileReader();
20
+ reader.onload = e => {
21
+ var _a;
22
+ try {
23
+ const text = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
24
+ if (typeof text === 'string')
25
+ resolve(JSON.parse(text));
26
+ }
27
+ catch (err) {
28
+ console.error('Error parsing prefab JSON:', err);
29
+ resolve(undefined);
30
+ }
31
+ };
32
+ reader.readAsText(file);
33
+ };
34
+ input.click();
35
+ });
36
+ }
1
37
  /** Find a node by ID in the tree */
2
38
  export function findNode(root, id) {
3
39
  var _a;
@@ -64,6 +100,11 @@ export function cloneNode(node) {
64
100
  var _a, _b;
65
101
  return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), name: `${(_a = node.name) !== null && _a !== void 0 ? _a : "Node"} Copy`, children: (_b = node.children) === null || _b === void 0 ? void 0 : _b.map(cloneNode) });
66
102
  }
103
+ /** Recursively update all IDs in a node tree */
104
+ export function regenerateIds(node) {
105
+ var _a;
106
+ return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(), children: (_a = node.children) === null || _a === void 0 ? void 0 : _a.map(regenerateIds) });
107
+ }
67
108
  /** Get component data from a node */
68
109
  export function getComponent(node, type) {
69
110
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/index.ts CHANGED
@@ -1,7 +1,18 @@
1
- // Components
1
+ // Core Components
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
+
4
+ // Prefab Editor
3
5
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
6
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
4
7
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
8
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
9
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
10
+ export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
11
+ export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
12
+ export * as editorStyles from './tools/prefabeditor/styles';
13
+ export * from './tools/prefabeditor/utils';
14
+
15
+ // Asset Tools
5
16
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
6
17
  export {
7
18
  TextureListViewer,
@@ -10,16 +21,5 @@ export {
10
21
  SharedCanvas,
11
22
  } from './tools/assetviewer/page';
12
23
 
13
- // Component Registry
14
- export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
15
- export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
16
-
17
- // Editor Styles & Utils
18
- export * as editorStyles from './tools/prefabeditor/styles';
19
- export * from './tools/prefabeditor/utils';
20
-
21
24
  // Helpers
22
25
  export * from './helpers';
23
-
24
- // Types
25
- export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ interface EditorContextType {
4
+ transformMode: "translate" | "rotate" | "scale";
5
+ setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
6
+ snapResolution: number;
7
+ setSnapResolution: (resolution: number) => void;
8
+ onScreenshot?: () => void;
9
+ onExportGLB?: () => void;
10
+ }
11
+
12
+ export const EditorContext = createContext<EditorContextType | null>(null);
13
+
14
+ export function useEditorContext() {
15
+ const context = useContext(EditorContext);
16
+ if (!context) {
17
+ throw new Error("useEditorContext must be used within EditorContext.Provider");
18
+ }
19
+ return context;
20
+ }
@@ -2,15 +2,14 @@ import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
2
2
  import { Prefab, GameObject } from "./types";
3
3
  import { getComponent } from './components/ComponentRegistry';
4
4
  import { base, tree, menu } from './styles';
5
- import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
5
+ import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
6
+ import { useEditorContext } from './EditorContext';
6
7
 
7
8
  export default function EditorTree({
8
9
  prefabData,
9
10
  setPrefabData,
10
11
  selectedId,
11
12
  setSelectedId,
12
- onSave,
13
- onLoad,
14
13
  onUndo,
15
14
  onRedo,
16
15
  canUndo,
@@ -20,8 +19,6 @@ export default function EditorTree({
20
19
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
21
20
  selectedId: string | null;
22
21
  setSelectedId: Dispatch<SetStateAction<string | null>>;
23
- onSave?: () => void;
24
- onLoad?: () => void;
25
22
  onUndo?: () => void;
26
23
  onRedo?: () => void;
27
24
  canUndo?: boolean;
@@ -212,23 +209,11 @@ export default function EditorTree({
212
209
 
213
210
  </button>
214
211
  {fileMenuOpen && (
215
- <div
216
- style={{ ...menu.container, top: 28, right: 0 }}
217
- onClick={(e) => e.stopPropagation()}
218
- >
219
- <button
220
- style={menu.item}
221
- onClick={() => { onLoad?.(); setFileMenuOpen(false); }}
222
- >
223
- 📥 Load
224
- </button>
225
- <button
226
- style={menu.item}
227
- onClick={() => { onSave?.(); setFileMenuOpen(false); }}
228
- >
229
- 💾 Save
230
- </button>
231
- </div>
212
+ <FileMenu
213
+ prefabData={prefabData}
214
+ setPrefabData={setPrefabData}
215
+ onClose={() => setFileMenuOpen(false)}
216
+ />
232
217
  )}
233
218
  </div>
234
219
  </div>
@@ -261,3 +246,79 @@ export default function EditorTree({
261
246
  </>
262
247
  );
263
248
  }
249
+
250
+ function FileMenu({
251
+ prefabData,
252
+ setPrefabData,
253
+ onClose
254
+ }: {
255
+ prefabData: Prefab;
256
+ setPrefabData: Dispatch<SetStateAction<Prefab>>;
257
+ onClose: () => void;
258
+ }) {
259
+ const { onScreenshot, onExportGLB } = useEditorContext();
260
+
261
+ const handleLoad = async () => {
262
+ const loadedPrefab = await loadJson();
263
+ if (!loadedPrefab) return;
264
+ setPrefabData(loadedPrefab);
265
+ onClose();
266
+ };
267
+
268
+ const handleSave = () => {
269
+ saveJson(prefabData, "prefab");
270
+ onClose();
271
+ };
272
+
273
+ const handleLoadIntoScene = async () => {
274
+ const loadedPrefab = await loadJson();
275
+ if (!loadedPrefab) return;
276
+
277
+ setPrefabData(prev => ({
278
+ ...prev,
279
+ root: updateNodeById(prev.root, prev.root.id, root => ({
280
+ ...root,
281
+ children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
282
+ }))
283
+ }));
284
+ onClose();
285
+ };
286
+
287
+ return (
288
+ <div
289
+ style={{ ...menu.container, top: 28, right: 0 }}
290
+ onClick={(e) => e.stopPropagation()}
291
+ >
292
+ <button
293
+ style={menu.item}
294
+ onClick={handleLoad}
295
+ >
296
+ 📥 Load Prefab JSON
297
+ </button>
298
+ <button
299
+ style={menu.item}
300
+ onClick={handleSave}
301
+ >
302
+ 💾 Save Prefab JSON
303
+ </button>
304
+ <button
305
+ style={menu.item}
306
+ onClick={handleLoadIntoScene}
307
+ >
308
+ 📂 Load into Scene
309
+ </button>
310
+ <button
311
+ style={menu.item}
312
+ onClick={() => { onScreenshot?.(); onClose(); }}
313
+ >
314
+ 📸 Screenshot
315
+ </button>
316
+ <button
317
+ style={menu.item}
318
+ onClick={() => { onExportGLB?.(); onClose(); }}
319
+ >
320
+ 📦 Export GLB
321
+ </button>
322
+ </div>
323
+ );
324
+ }