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.
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -5
- package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
- package/dist/tools/prefabeditor/EditorContext.js +9 -0
- package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
- package/dist/tools/prefabeditor/EditorTree.js +38 -3
- package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
- package/dist/tools/prefabeditor/EditorUI.js +4 -2
- package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
- package/dist/tools/prefabeditor/ExportHelper.js +55 -0
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
- package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +7 -2
- package/dist/tools/prefabeditor/PrefabRoot.js +78 -49
- package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
- package/dist/tools/prefabeditor/components/Input.js +9 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
- package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
- package/dist/tools/prefabeditor/utils.d.ts +7 -1
- package/dist/tools/prefabeditor/utils.js +41 -0
- package/package.json +1 -1
- package/src/index.ts +12 -12
- package/src/tools/prefabeditor/EditorContext.tsx +20 -0
- package/src/tools/prefabeditor/EditorTree.tsx +83 -22
- package/src/tools/prefabeditor/EditorUI.tsx +2 -10
- package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
- package/src/tools/prefabeditor/PrefabEditor.tsx +80 -51
- package/src/tools/prefabeditor/PrefabRoot.tsx +181 -69
- package/src/tools/prefabeditor/components/Input.tsx +11 -3
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
- package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
- 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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
165
|
-
const
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}) })
|
|
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
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
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|