react-three-game 0.0.52 → 0.0.53
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/helpers/index.js +1 -1
- package/dist/tools/prefabeditor/EditorTree.js +48 -7
- package/dist/tools/prefabeditor/EditorUI.js +2 -2
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +4 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -3
- package/dist/tools/prefabeditor/PrefabRoot.js +9 -3
- package/dist/tools/prefabeditor/components/GeometryComponent.js +8 -8
- package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
- package/dist/tools/prefabeditor/components/Input.js +63 -11
- package/dist/tools/prefabeditor/components/MaterialComponent.js +4 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +3 -1
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +10 -2
- package/dist/tools/prefabeditor/styles.d.ts +1 -4
- package/dist/tools/prefabeditor/styles.js +2 -0
- package/package.json +7 -7
- package/src/helpers/index.ts +1 -1
- package/src/tools/prefabeditor/EditorTree.tsx +92 -17
- package/src/tools/prefabeditor/EditorUI.tsx +2 -2
- package/src/tools/prefabeditor/PrefabEditor.tsx +24 -15
- package/src/tools/prefabeditor/PrefabRoot.tsx +14 -6
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +13 -13
- package/src/tools/prefabeditor/components/Input.tsx +110 -20
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +22 -6
- package/src/tools/prefabeditor/components/ModelComponent.tsx +9 -1
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +10 -2
- package/src/tools/prefabeditor/styles.ts +3 -1
package/dist/helpers/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Physics (fixed by default)
|
|
9
9
|
*/
|
|
10
10
|
export function ground(options = {}) {
|
|
11
|
-
const { id = "ground", size = 50, position = [0, 0, 0], rotation = [-Math.PI / 2, 0, 0], scale = [1, 1, 1], color = "
|
|
11
|
+
const { id = "ground", size = 50, position = [0, 0, 0], rotation = [-Math.PI / 2, 0, 0], scale = [1, 1, 1], color = "#eeeeee", texture, repeat = texture ? true : false, repeatCount = [25, 25], physicsType = "fixed", disabled = false, } = options;
|
|
12
12
|
return {
|
|
13
13
|
id,
|
|
14
14
|
disabled,
|
|
@@ -19,6 +19,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
19
19
|
const [collapsedIds, setCollapsedIds] = useState(new Set());
|
|
20
20
|
const [collapsed, setCollapsed] = useState(false);
|
|
21
21
|
const [fileMenuOpen, setFileMenuOpen] = useState(false);
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
22
23
|
if (!prefabData || !setPrefabData)
|
|
23
24
|
return null;
|
|
24
25
|
const handleContextMenu = (e, nodeId) => {
|
|
@@ -73,6 +74,10 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
73
74
|
setSelectedId(null);
|
|
74
75
|
setContextMenu(null);
|
|
75
76
|
};
|
|
77
|
+
const handleToggleDisabled = (nodeId) => {
|
|
78
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { disabled: !node.disabled }))) })));
|
|
79
|
+
setContextMenu(null);
|
|
80
|
+
};
|
|
76
81
|
const handleDragStart = (e, id) => {
|
|
77
82
|
if (id === prefabData.root.id)
|
|
78
83
|
return e.preventDefault();
|
|
@@ -105,23 +110,59 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
105
110
|
});
|
|
106
111
|
setDraggedId(null);
|
|
107
112
|
};
|
|
113
|
+
const matchesSearch = (node, query) => {
|
|
114
|
+
var _a, _b, _c;
|
|
115
|
+
if (!query)
|
|
116
|
+
return true;
|
|
117
|
+
const lowerQuery = query.toLowerCase();
|
|
118
|
+
const nodeName = ((_a = node.name) !== null && _a !== void 0 ? _a : node.id).toLowerCase();
|
|
119
|
+
if (nodeName.includes(lowerQuery))
|
|
120
|
+
return true;
|
|
121
|
+
return (_c = (_b = node.children) === null || _b === void 0 ? void 0 : _b.some(child => matchesSearch(child, query))) !== null && _c !== void 0 ? _c : false;
|
|
122
|
+
};
|
|
108
123
|
const renderNode = (node, depth = 0) => {
|
|
109
124
|
var _a;
|
|
110
125
|
if (!node)
|
|
111
126
|
return null;
|
|
127
|
+
if (!matchesSearch(node, searchQuery))
|
|
128
|
+
return null;
|
|
112
129
|
const isSelected = node.id === selectedId;
|
|
113
130
|
const isCollapsed = collapsedIds.has(node.id);
|
|
114
131
|
const hasChildren = node.children && node.children.length > 0;
|
|
115
132
|
const isRoot = node.id === prefabData.root.id;
|
|
116
|
-
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => setDraggedId(null), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
|
|
134
|
+
width: 12,
|
|
135
|
+
opacity: 0.6,
|
|
136
|
+
marginRight: 4,
|
|
137
|
+
cursor: 'pointer',
|
|
138
|
+
visibility: hasChildren ? 'visible' : 'hidden'
|
|
139
|
+
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isRoot && (_jsx("button", { style: {
|
|
140
|
+
background: 'none',
|
|
141
|
+
border: 'none',
|
|
120
142
|
cursor: 'pointer',
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
padding: '0 4px',
|
|
144
|
+
fontSize: 14,
|
|
145
|
+
opacity: node.disabled ? 0.5 : 0.7,
|
|
146
|
+
color: 'inherit',
|
|
147
|
+
}, onClick: (e) => {
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
handleToggleDisabled(node.id);
|
|
150
|
+
}, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
123
151
|
};
|
|
124
|
-
return (_jsxs(_Fragment, { children: [
|
|
152
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
|
|
153
|
+
.tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
154
|
+
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
155
|
+
.tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
156
|
+
` }), _jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 6px', borderBottom: '1px solid rgba(255,255,255,0.1)' }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: {
|
|
157
|
+
width: '100%',
|
|
158
|
+
padding: '4px 8px',
|
|
159
|
+
background: 'rgba(255,255,255,0.05)',
|
|
160
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
161
|
+
borderRadius: 3,
|
|
162
|
+
color: 'inherit',
|
|
163
|
+
fontSize: 11,
|
|
164
|
+
outline: 'none',
|
|
165
|
+
} }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
|
|
125
166
|
}
|
|
126
167
|
function FileMenu({ prefabData, setPrefabData, onClose }) {
|
|
127
168
|
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
@@ -47,13 +47,13 @@ function NodeInspector({ node, updateNode, deleteNode, basePath }) {
|
|
|
47
47
|
if (!newAvailable.includes(addType))
|
|
48
48
|
setAddType(newAvailable[0] || "");
|
|
49
49
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
50
|
-
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
50
|
+
return _jsxs("div", { style: Object.assign(Object.assign({}, inspector.content), { paddingRight: 2 }), className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }, children: [_jsx("div", { style: { fontSize: 10, color: '#888', wordBreak: 'break-all', border: '1px solid rgba(255,255,255,0.1)', padding: '2px 4px', borderRadius: 4, flex: 1 }, children: node.id }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), title: "Delete Node", onClick: deleteNode, children: "\u274C" })] }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", placeholder: 'Node name', onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsx("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: _jsx("div", { style: base.label, children: "Components" }) }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
51
51
|
if (!comp)
|
|
52
52
|
return null;
|
|
53
53
|
const def = ALL_COMPONENTS[comp.type];
|
|
54
54
|
if (!def)
|
|
55
55
|
return _jsxs("div", { style: { color: '#ff8888', fontSize: 11 }, children: ["Unknown: ", comp.type] }, key);
|
|
56
|
-
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
|
|
56
|
+
return (_jsxs("div", { style: { marginBottom: 8, backgroundColor: 'rgba(255, 255, 255, 0.1)', padding: 8, borderRadius: 4 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 500 }, children: key }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px' }), title: "Remove Component", onClick: () => updateNode(n => {
|
|
57
57
|
const _a = n.components || {}, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
58
58
|
return Object.assign(Object.assign({}, n), { components: rest });
|
|
59
59
|
}), children: "\u2715" })] }), def.Editor && (_jsx(def.Editor, { component: comp, node: node, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath }))] }, key));
|
|
@@ -10,6 +10,7 @@ export interface PrefabEditorRef {
|
|
|
10
10
|
declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
|
|
11
11
|
basePath?: string;
|
|
12
12
|
initialPrefab?: Prefab;
|
|
13
|
+
physics?: boolean;
|
|
13
14
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
14
15
|
children?: React.ReactNode;
|
|
15
16
|
} & import("react").RefAttributes<PrefabEditorRef>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import GameCanvas from "../../shared/GameCanvas";
|
|
3
3
|
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
|
4
4
|
import PrefabRoot from "./PrefabRoot";
|
|
@@ -20,7 +20,7 @@ const DEFAULT_PREFAB = {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
|
|
23
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
|
|
24
24
|
const [editMode, setEditMode] = useState(true);
|
|
25
25
|
const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
|
|
26
26
|
const [selectedId, setSelectedId] = useState(null);
|
|
@@ -118,6 +118,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, chil
|
|
|
118
118
|
setPrefab: setLoadedPrefab,
|
|
119
119
|
rootRef: prefabRootRef
|
|
120
120
|
}), [loadedPrefab]);
|
|
121
|
+
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] }));
|
|
121
122
|
return _jsxs(EditorContext.Provider, { value: {
|
|
122
123
|
transformMode,
|
|
123
124
|
setTransformMode,
|
|
@@ -125,7 +126,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, chil
|
|
|
125
126
|
setSnapResolution,
|
|
126
127
|
onScreenshot: handleScreenshot,
|
|
127
128
|
onExportGLB: handleExportGLB
|
|
128
|
-
}, children: [_jsx(GameCanvas, { children:
|
|
129
|
+
}, children: [_jsx(GameCanvas, { children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content }), _jsx("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
|
|
129
130
|
});
|
|
130
131
|
PrefabEditor.displayName = "PrefabEditor";
|
|
131
132
|
export default PrefabEditor;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Group, Matrix4, Object3D, Texture } from "three";
|
|
2
2
|
import { ThreeEvent } from "@react-three/fiber";
|
|
3
3
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
4
|
-
import type { RapierRigidBody } from "@react-three/rapier";
|
|
5
4
|
export interface PrefabRootRef {
|
|
6
5
|
root: Group | null;
|
|
7
|
-
rigidBodyRefs: Map<string,
|
|
6
|
+
rigidBodyRefs: Map<string, any>;
|
|
8
7
|
}
|
|
9
8
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
|
|
10
9
|
editMode?: boolean;
|
|
@@ -22,7 +21,7 @@ interface RendererProps {
|
|
|
22
21
|
onSelect?: (id: string) => void;
|
|
23
22
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
24
23
|
registerRef: (id: string, obj: Object3D | null) => void;
|
|
25
|
-
registerRigidBodyRef: (id: string, rb:
|
|
24
|
+
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
26
25
|
loadedModels: Record<string, Object3D>;
|
|
27
26
|
loadedTextures: Record<string, Texture>;
|
|
28
27
|
editMode?: boolean;
|
|
@@ -99,12 +99,18 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
99
99
|
if (textures[file] || loading.current.has(file))
|
|
100
100
|
return;
|
|
101
101
|
loading.current.add(file);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// Handle full URLs (http/https) or regular paths
|
|
103
|
+
const path = file.startsWith("http://") || file.startsWith("https://")
|
|
104
|
+
? file
|
|
105
|
+
: file.startsWith("/")
|
|
106
|
+
? `${basePath}${file}`
|
|
107
|
+
: `${basePath}/${file}`;
|
|
105
108
|
loader.load(path, tex => {
|
|
106
109
|
tex.colorSpace = SRGBColorSpace;
|
|
107
110
|
setTextures(t => (Object.assign(Object.assign({}, t), { [file]: tex })));
|
|
111
|
+
}, undefined, (err) => {
|
|
112
|
+
console.error(`Failed to load texture: ${path}`, err);
|
|
113
|
+
loading.current.delete(file);
|
|
108
114
|
});
|
|
109
115
|
});
|
|
110
116
|
}, [data, models, textures]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import { FieldRenderer, Input
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FieldRenderer, Input } from "./Input";
|
|
3
3
|
const GEOMETRY_ARGS = {
|
|
4
4
|
box: {
|
|
5
5
|
labels: ["Width", "Height", "Depth"],
|
|
@@ -41,13 +41,13 @@ function GeometryComponentEditor({ component, onUpdate, }) {
|
|
|
41
41
|
const currentType = values.geometryType;
|
|
42
42
|
const currentSchema = GEOMETRY_ARGS[currentType];
|
|
43
43
|
const currentArgs = values.args || currentSchema.defaults;
|
|
44
|
-
return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap:
|
|
44
|
+
return (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: currentSchema.labels.map((label, i) => {
|
|
45
45
|
var _a;
|
|
46
|
-
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
return (_jsx(Input, { label: label, value: (_a = currentArgs[i]) !== null && _a !== void 0 ? _a : currentSchema.defaults[i], step: 0.1, min: 0.01, onChange: value => {
|
|
47
|
+
const next = [...currentArgs];
|
|
48
|
+
next[i] = value;
|
|
49
|
+
onChangeMultiple({ args: next });
|
|
50
|
+
} }, label));
|
|
51
51
|
}) }));
|
|
52
52
|
},
|
|
53
53
|
},
|
|
@@ -52,8 +52,9 @@ interface InputProps {
|
|
|
52
52
|
min?: number;
|
|
53
53
|
max?: number;
|
|
54
54
|
style?: React.CSSProperties;
|
|
55
|
+
label?: string;
|
|
55
56
|
}
|
|
56
|
-
export declare function Input({ value, onChange, step, min, max, style }: InputProps): import("react/jsx-runtime").JSX.Element;
|
|
57
|
+
export declare function Input({ value, onChange, step, min, max, style, label }: InputProps): import("react/jsx-runtime").JSX.Element;
|
|
57
58
|
export declare function Label({ children }: {
|
|
58
59
|
children: React.ReactNode;
|
|
59
60
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
6
6
|
// Shared styles
|
|
7
7
|
const styles = {
|
|
8
8
|
input: {
|
|
9
|
-
width: '
|
|
9
|
+
width: '80px',
|
|
10
10
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
11
11
|
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
12
12
|
padding: '2px 4px',
|
|
@@ -14,17 +14,18 @@ const styles = {
|
|
|
14
14
|
color: 'rgba(165, 243, 252, 1)',
|
|
15
15
|
fontFamily: 'monospace',
|
|
16
16
|
outline: 'none',
|
|
17
|
+
textAlign: 'right',
|
|
17
18
|
},
|
|
18
19
|
label: {
|
|
19
20
|
display: 'block',
|
|
20
21
|
fontSize: '9px',
|
|
21
|
-
color: 'rgba(34, 211, 238, 0.
|
|
22
|
+
color: 'rgba(34, 211, 238, 0.9)',
|
|
22
23
|
textTransform: 'uppercase',
|
|
23
24
|
letterSpacing: '0.05em',
|
|
24
25
|
marginBottom: 2,
|
|
25
26
|
},
|
|
26
27
|
};
|
|
27
|
-
export function Input({ value, onChange, step, min, max, style }) {
|
|
28
|
+
export function Input({ value, onChange, step, min, max, style, label }) {
|
|
28
29
|
const [draft, setDraft] = useState(() => value.toString());
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
setDraft(value.toString());
|
|
@@ -43,6 +44,55 @@ export function Input({ value, onChange, step, min, max, style }) {
|
|
|
43
44
|
setDraft(value.toString());
|
|
44
45
|
}
|
|
45
46
|
};
|
|
47
|
+
const dragState = useRef(null);
|
|
48
|
+
const startScrub = (e) => {
|
|
49
|
+
if (!label)
|
|
50
|
+
return;
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
dragState.current = {
|
|
53
|
+
startX: e.clientX,
|
|
54
|
+
startValue: value
|
|
55
|
+
};
|
|
56
|
+
e.target.setPointerCapture(e.pointerId);
|
|
57
|
+
document.body.style.cursor = "ew-resize";
|
|
58
|
+
};
|
|
59
|
+
const onScrubMove = (e) => {
|
|
60
|
+
if (!dragState.current)
|
|
61
|
+
return;
|
|
62
|
+
const { startX, startValue } = dragState.current;
|
|
63
|
+
const dx = e.clientX - startX;
|
|
64
|
+
let speed = 0.02;
|
|
65
|
+
if (e.shiftKey)
|
|
66
|
+
speed *= 0.1; // fine
|
|
67
|
+
if (e.altKey)
|
|
68
|
+
speed *= 5; // coarse
|
|
69
|
+
let nextValue = startValue + dx * speed;
|
|
70
|
+
// Apply min/max constraints
|
|
71
|
+
if (min !== undefined && nextValue < min)
|
|
72
|
+
nextValue = min;
|
|
73
|
+
if (max !== undefined && nextValue > max)
|
|
74
|
+
nextValue = max;
|
|
75
|
+
setDraft(nextValue.toFixed(3));
|
|
76
|
+
onChange(nextValue);
|
|
77
|
+
};
|
|
78
|
+
const endScrub = (e) => {
|
|
79
|
+
if (!dragState.current)
|
|
80
|
+
return;
|
|
81
|
+
dragState.current = null;
|
|
82
|
+
document.body.style.cursor = "";
|
|
83
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
84
|
+
};
|
|
85
|
+
if (label) {
|
|
86
|
+
return (_jsxs("div", { style: {
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
justifyContent: 'space-between',
|
|
90
|
+
}, children: [_jsx("span", { style: Object.assign(Object.assign({}, styles.label), { marginBottom: 0, cursor: 'ew-resize', userSelect: 'none', flex: '0 0 auto', minWidth: 20 }), onPointerDown: startScrub, onPointerMove: onScrubMove, onPointerUp: endScrub, children: label }), _jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
|
|
91
|
+
if (e.key === 'Enter') {
|
|
92
|
+
e.target.blur();
|
|
93
|
+
}
|
|
94
|
+
}, step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) })] }));
|
|
95
|
+
}
|
|
46
96
|
return (_jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
|
|
47
97
|
if (e.key === 'Enter') {
|
|
48
98
|
e.target.blur();
|
|
@@ -156,20 +206,22 @@ export function Vector3Input({ label, value, onChange, snap }) {
|
|
|
156
206
|
// Additional Input Components
|
|
157
207
|
// ============================================================================
|
|
158
208
|
export function ColorInput({ label, value, onChange }) {
|
|
159
|
-
return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap:
|
|
160
|
-
height:
|
|
161
|
-
width:
|
|
209
|
+
return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap: 4, justifyContent: 'space-between' }, children: [_jsx("input", { type: "color", style: {
|
|
210
|
+
height: 32,
|
|
211
|
+
width: 48,
|
|
162
212
|
backgroundColor: 'transparent',
|
|
163
|
-
border: '
|
|
213
|
+
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
214
|
+
borderRadius: 4,
|
|
164
215
|
cursor: 'pointer',
|
|
165
216
|
padding: 0,
|
|
166
|
-
|
|
217
|
+
flexShrink: 0,
|
|
218
|
+
}, value: value, onChange: e => onChange(e.target.value) }), _jsx("input", { type: "text", style: Object.assign({}, styles.input), value: value, onChange: e => onChange(e.target.value) })] })] }));
|
|
167
219
|
}
|
|
168
220
|
export function StringInput({ label, value, onChange, placeholder }) {
|
|
169
221
|
return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "text", style: styles.input, value: value, onChange: e => onChange(e.target.value), placeholder: placeholder })] }));
|
|
170
222
|
}
|
|
171
223
|
export function BooleanInput({ label, value, onChange }) {
|
|
172
|
-
return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
|
|
224
|
+
return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("input", { type: "checkbox", style: {
|
|
173
225
|
height: 16,
|
|
174
226
|
width: 16,
|
|
175
227
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
@@ -178,7 +230,7 @@ export function BooleanInput({ label, value, onChange }) {
|
|
|
178
230
|
}, checked: value, onChange: e => onChange(e.target.checked) })] }));
|
|
179
231
|
}
|
|
180
232
|
export function SelectInput({ label, value, onChange, options }) {
|
|
181
|
-
return (_jsxs("div", { children: [label && _jsx(Label, { children: label }), _jsx("select", { style: styles.input, value: value, onChange: e => onChange(e.target.value), children: options.map(opt => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
233
|
+
return (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between' }, children: [label && _jsx(Label, { children: label }), _jsx("select", { style: styles.input, value: value, onChange: e => onChange(e.target.value), children: options.map(opt => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
182
234
|
}
|
|
183
235
|
export function FieldRenderer({ fields, values, onChange }) {
|
|
184
236
|
const updateField = (name, value) => {
|
|
@@ -191,7 +243,7 @@ export function FieldRenderer({ fields, values, onChange }) {
|
|
|
191
243
|
case 'vector3':
|
|
192
244
|
return (_jsx(Vector3Input, { label: field.label, value: value !== null && value !== void 0 ? value : [0, 0, 0], onChange: v => updateField(field.name, v), snap: field.snap }, field.name));
|
|
193
245
|
case 'number':
|
|
194
|
-
return (
|
|
246
|
+
return (_jsx(Input, { label: field.label, value: value !== null && value !== void 0 ? value : 0, onChange: v => updateField(field.name, v), min: field.min, max: field.max, step: field.step }, field.name));
|
|
195
247
|
case 'string':
|
|
196
248
|
return (_jsx(StringInput, { label: field.label, value: value !== null && value !== void 0 ? value : '', onChange: v => updateField(field.name, v), placeholder: field.placeholder }, field.name));
|
|
197
249
|
case 'color':
|
|
@@ -25,7 +25,9 @@ function TexturePicker({ value, onChange, basePath }) {
|
|
|
25
25
|
.then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
|
|
26
26
|
.catch(console.error);
|
|
27
27
|
}, [basePath]);
|
|
28
|
-
return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: value || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? '
|
|
28
|
+
return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: value || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
|
|
29
|
+
onChange(undefined);
|
|
30
|
+
}, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(TextureListViewer, { files: textureFiles, selected: value || undefined, onSelect: (file) => {
|
|
29
31
|
onChange(file);
|
|
30
32
|
setShowPicker(false);
|
|
31
33
|
}, basePath: basePath }) }))] }));
|
|
@@ -58,7 +60,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
58
60
|
label: 'Repeat (X, Y)',
|
|
59
61
|
render: ({ value, onChange }) => {
|
|
60
62
|
var _a, _b;
|
|
61
|
-
return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); } }), _jsx(Input, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); } })] }));
|
|
63
|
+
return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { label: "X", value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0.01, max: 100, step: 0.1 }), _jsx(Input, { label: "Y", value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0.01, max: 100, step: 0.1 })] }));
|
|
62
64
|
},
|
|
63
65
|
}] : []),
|
|
64
66
|
{ name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
|
|
@@ -17,7 +17,9 @@ function ModelPicker({ value, onChange, basePath, nodeId }) {
|
|
|
17
17
|
onChange(filename);
|
|
18
18
|
setShowPicker(false);
|
|
19
19
|
};
|
|
20
|
-
return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? '
|
|
20
|
+
return (_jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: value ? `/${value}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Cancel' : 'Change' }), _jsx("button", { onClick: () => {
|
|
21
|
+
onChange(undefined);
|
|
22
|
+
}, style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }, children: "Clear" }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(ModelListViewer, { files: modelFiles, selected: value ? `/${value}` : undefined, onSelect: handleModelSelect, basePath: basePath }, nodeId) }))] }));
|
|
21
23
|
}
|
|
22
24
|
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
|
|
23
25
|
const fields = [
|
|
@@ -99,7 +99,15 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
|
|
|
99
99
|
const { type, colliders, sensor, activeCollisionTypes } = properties, otherProps = __rest(properties, ["type", "colliders", "sensor", "activeCollisionTypes"]);
|
|
100
100
|
const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
|
|
101
101
|
const rigidBodyRef = useRef(null);
|
|
102
|
-
|
|
102
|
+
// Try to get rapier context - will be null if not inside <Physics>
|
|
103
|
+
let rapier = null;
|
|
104
|
+
try {
|
|
105
|
+
const rapierContext = useRapier();
|
|
106
|
+
rapier = rapierContext.rapier;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
// Not inside Physics context - that's ok, just won't have rapier features
|
|
110
|
+
}
|
|
103
111
|
// Register RigidBody ref when it's available
|
|
104
112
|
useEffect(() => {
|
|
105
113
|
if (nodeId && registerRigidBodyRef && rigidBodyRef.current) {
|
|
@@ -113,7 +121,7 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
|
|
|
113
121
|
}, [nodeId, registerRigidBodyRef]);
|
|
114
122
|
// Configure active collision types for kinematic/sensor bodies
|
|
115
123
|
useEffect(() => {
|
|
116
|
-
if (activeCollisionTypes === 'all' && rigidBodyRef.current) {
|
|
124
|
+
if (activeCollisionTypes === 'all' && rigidBodyRef.current && rapier) {
|
|
117
125
|
const rb = rigidBodyRef.current;
|
|
118
126
|
// Apply to all colliders on this rigid body
|
|
119
127
|
for (let i = 0; i < rb.numColliders(); i++) {
|
|
@@ -1755,10 +1755,7 @@ export declare const tree: {
|
|
|
1755
1755
|
colorRendering?: import("csstype").Property.ColorRendering | undefined;
|
|
1756
1756
|
glyphOrientationVertical?: import("csstype").Property.GlyphOrientationVertical | undefined;
|
|
1757
1757
|
};
|
|
1758
|
-
scroll:
|
|
1759
|
-
overflowY: "auto";
|
|
1760
|
-
padding: number;
|
|
1761
|
-
};
|
|
1758
|
+
scroll: React.CSSProperties;
|
|
1762
1759
|
row: React.CSSProperties;
|
|
1763
1760
|
selected: {
|
|
1764
1761
|
background: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"description": "Batteries included React Three Fiber game engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -29,19 +29,19 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@react-three/drei": "^10.7.7",
|
|
32
|
-
"@react-three/fiber": "^9.
|
|
32
|
+
"@react-three/fiber": "^9.5.0",
|
|
33
33
|
"@react-three/rapier": "^2.2.0",
|
|
34
|
-
"@types/react": "^19.2.
|
|
34
|
+
"@types/react": "^19.2.9",
|
|
35
35
|
"@types/react-dom": "^19.2.3",
|
|
36
|
-
"@types/three": "^0.
|
|
36
|
+
"@types/three": "^0.182.0",
|
|
37
37
|
"concurrently": "^9.2.1",
|
|
38
|
-
"react": "^19.2.
|
|
39
|
-
"react-dom": "^19.2.
|
|
38
|
+
"react": "^19.2.4",
|
|
39
|
+
"react-dom": "^19.2.4",
|
|
40
40
|
"three": "^0.182.0",
|
|
41
41
|
"typescript": "^5.9.3",
|
|
42
42
|
"vite": "^7.3.1"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"react-error-boundary": "^6.
|
|
45
|
+
"react-error-boundary": "^6.1.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function ground(options: GroundOptions = {}): GameObject {
|
|
|
45
45
|
position = [0, 0, 0],
|
|
46
46
|
rotation = [-Math.PI / 2, 0, 0],
|
|
47
47
|
scale = [1, 1, 1],
|
|
48
|
-
color = "
|
|
48
|
+
color = "#eeeeee",
|
|
49
49
|
texture,
|
|
50
50
|
repeat = texture ? true : false,
|
|
51
51
|
repeatCount = [25, 25],
|
|
@@ -29,6 +29,7 @@ export default function EditorTree({
|
|
|
29
29
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
30
30
|
const [collapsed, setCollapsed] = useState(false);
|
|
31
31
|
const [fileMenuOpen, setFileMenuOpen] = useState(false);
|
|
32
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
32
33
|
|
|
33
34
|
if (!prefabData || !setPrefabData) return null;
|
|
34
35
|
|
|
@@ -91,6 +92,17 @@ export default function EditorTree({
|
|
|
91
92
|
setContextMenu(null);
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
const handleToggleDisabled = (nodeId: string) => {
|
|
96
|
+
setPrefabData(prev => ({
|
|
97
|
+
...prev,
|
|
98
|
+
root: updateNodeById(prev.root, nodeId, node => ({
|
|
99
|
+
...node,
|
|
100
|
+
disabled: !node.disabled
|
|
101
|
+
}))
|
|
102
|
+
}));
|
|
103
|
+
setContextMenu(null);
|
|
104
|
+
};
|
|
105
|
+
|
|
94
106
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
95
107
|
if (id === prefabData.root.id) return e.preventDefault();
|
|
96
108
|
e.dataTransfer.effectAllowed = "move";
|
|
@@ -128,8 +140,17 @@ export default function EditorTree({
|
|
|
128
140
|
};
|
|
129
141
|
|
|
130
142
|
|
|
143
|
+
const matchesSearch = (node: GameObject, query: string): boolean => {
|
|
144
|
+
if (!query) return true;
|
|
145
|
+
const lowerQuery = query.toLowerCase();
|
|
146
|
+
const nodeName = (node.name ?? node.id).toLowerCase();
|
|
147
|
+
if (nodeName.includes(lowerQuery)) return true;
|
|
148
|
+
return node.children?.some(child => matchesSearch(child, query)) ?? false;
|
|
149
|
+
};
|
|
150
|
+
|
|
131
151
|
const renderNode = (node: GameObject, depth = 0): React.ReactNode => {
|
|
132
152
|
if (!node) return null;
|
|
153
|
+
if (!matchesSearch(node, searchQuery)) return null;
|
|
133
154
|
|
|
134
155
|
const isSelected = node.id === selectedId;
|
|
135
156
|
const isCollapsed = collapsedIds.has(node.id);
|
|
@@ -143,6 +164,10 @@ export default function EditorTree({
|
|
|
143
164
|
...tree.row,
|
|
144
165
|
...(isSelected ? tree.selected : {}),
|
|
145
166
|
paddingLeft: `${depth * 12 + 6}px`,
|
|
167
|
+
opacity: node.disabled ? 0.4 : 1,
|
|
168
|
+
display: 'flex',
|
|
169
|
+
alignItems: 'center',
|
|
170
|
+
justifyContent: 'space-between',
|
|
146
171
|
}}
|
|
147
172
|
draggable={!isRoot}
|
|
148
173
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
@@ -152,22 +177,44 @@ export default function EditorTree({
|
|
|
152
177
|
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
153
178
|
onDrop={(e) => handleDrop(e, node.id)}
|
|
154
179
|
>
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{
|
|
170
|
-
|
|
180
|
+
<div style={{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }}>
|
|
181
|
+
<span
|
|
182
|
+
style={{
|
|
183
|
+
width: 12,
|
|
184
|
+
opacity: 0.6,
|
|
185
|
+
marginRight: 4,
|
|
186
|
+
cursor: 'pointer',
|
|
187
|
+
visibility: hasChildren ? 'visible' : 'hidden'
|
|
188
|
+
}}
|
|
189
|
+
onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
|
|
190
|
+
>
|
|
191
|
+
{isCollapsed ? '▶' : '▼'}
|
|
192
|
+
</span>
|
|
193
|
+
{!isRoot && <span style={{ marginRight: 4, opacity: 0.4 }}>⋮⋮</span>}
|
|
194
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
195
|
+
{node.name ?? node.id}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
{!isRoot && (
|
|
199
|
+
<button
|
|
200
|
+
style={{
|
|
201
|
+
background: 'none',
|
|
202
|
+
border: 'none',
|
|
203
|
+
cursor: 'pointer',
|
|
204
|
+
padding: '0 4px',
|
|
205
|
+
fontSize: 14,
|
|
206
|
+
opacity: node.disabled ? 0.5 : 0.7,
|
|
207
|
+
color: 'inherit',
|
|
208
|
+
}}
|
|
209
|
+
onClick={(e) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
handleToggleDisabled(node.id);
|
|
212
|
+
}}
|
|
213
|
+
title={node.disabled ? 'Enable' : 'Disable'}
|
|
214
|
+
>
|
|
215
|
+
{node.disabled ? '◎' : '◉'}
|
|
216
|
+
</button>
|
|
217
|
+
)}
|
|
171
218
|
</div>
|
|
172
219
|
{!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
|
|
173
220
|
</div>
|
|
@@ -176,6 +223,11 @@ export default function EditorTree({
|
|
|
176
223
|
|
|
177
224
|
return (
|
|
178
225
|
<>
|
|
226
|
+
<style>{`
|
|
227
|
+
.tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
228
|
+
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
229
|
+
.tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
230
|
+
`}</style>
|
|
179
231
|
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => { setContextMenu(null); setFileMenuOpen(false); }}>
|
|
180
232
|
<div style={base.header}>
|
|
181
233
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
|
|
@@ -219,7 +271,30 @@ export default function EditorTree({
|
|
|
219
271
|
</div>
|
|
220
272
|
)}
|
|
221
273
|
</div>
|
|
222
|
-
{!collapsed &&
|
|
274
|
+
{!collapsed && (
|
|
275
|
+
<>
|
|
276
|
+
<div style={{ padding: '4px 6px', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
|
277
|
+
<input
|
|
278
|
+
type="text"
|
|
279
|
+
placeholder="Search nodes..."
|
|
280
|
+
value={searchQuery}
|
|
281
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
282
|
+
onClick={(e) => e.stopPropagation()}
|
|
283
|
+
style={{
|
|
284
|
+
width: '100%',
|
|
285
|
+
padding: '4px 8px',
|
|
286
|
+
background: 'rgba(255,255,255,0.05)',
|
|
287
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
288
|
+
borderRadius: 3,
|
|
289
|
+
color: 'inherit',
|
|
290
|
+
fontSize: 11,
|
|
291
|
+
outline: 'none',
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="tree-scroll" style={tree.scroll}>{renderNode(prefabData.root)}</div>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
223
298
|
</div>
|
|
224
299
|
|
|
225
300
|
{contextMenu && (
|
|
@@ -102,7 +102,7 @@ function NodeInspector({
|
|
|
102
102
|
if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
|
|
103
103
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
104
104
|
|
|
105
|
-
return <div style={inspector.content} className="prefab-scroll">
|
|
105
|
+
return <div style={{ ...inspector.content, paddingRight: 2 }} className="prefab-scroll">
|
|
106
106
|
{/* Node Name */}
|
|
107
107
|
<div style={base.section}>
|
|
108
108
|
<div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
|
|
@@ -136,7 +136,7 @@ function NodeInspector({
|
|
|
136
136
|
</div>;
|
|
137
137
|
|
|
138
138
|
return (
|
|
139
|
-
<div key={key} style={{ marginBottom: 8 }}>
|
|
139
|
+
<div key={key} style={{ marginBottom: 8, backgroundColor: 'rgba(255, 255, 255, 0.1)', padding: 8, borderRadius: 4 }}>
|
|
140
140
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
|
141
141
|
<div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
|
|
142
142
|
<button
|
|
@@ -33,9 +33,10 @@ const DEFAULT_PREFAB: Prefab = {
|
|
|
33
33
|
const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
34
34
|
basePath?: string;
|
|
35
35
|
initialPrefab?: Prefab;
|
|
36
|
+
physics?: boolean;
|
|
36
37
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
37
38
|
children?: React.ReactNode;
|
|
38
|
-
}>(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
|
|
39
|
+
}>(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
|
|
39
40
|
const [editMode, setEditMode] = useState(true);
|
|
40
41
|
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
|
|
41
42
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
@@ -132,6 +133,23 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
132
133
|
rootRef: prefabRootRef
|
|
133
134
|
}), [loadedPrefab]);
|
|
134
135
|
|
|
136
|
+
const content = (
|
|
137
|
+
<>
|
|
138
|
+
<ambientLight intensity={1.5} />
|
|
139
|
+
<gridHelper args={[10, 10]} position={[0, -1, 0]} />
|
|
140
|
+
<PrefabRoot
|
|
141
|
+
ref={prefabRootRef}
|
|
142
|
+
data={loadedPrefab}
|
|
143
|
+
editMode={editMode}
|
|
144
|
+
onPrefabChange={updatePrefab}
|
|
145
|
+
selectedId={selectedId}
|
|
146
|
+
onSelect={setSelectedId}
|
|
147
|
+
basePath={basePath}
|
|
148
|
+
/>
|
|
149
|
+
{children}
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
|
|
135
153
|
return <EditorContext.Provider value={{
|
|
136
154
|
transformMode,
|
|
137
155
|
setTransformMode,
|
|
@@ -141,20 +159,11 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
141
159
|
onExportGLB: handleExportGLB
|
|
142
160
|
}}>
|
|
143
161
|
<GameCanvas>
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
data={loadedPrefab}
|
|
150
|
-
editMode={editMode}
|
|
151
|
-
onPrefabChange={updatePrefab}
|
|
152
|
-
selectedId={selectedId}
|
|
153
|
-
onSelect={setSelectedId}
|
|
154
|
-
basePath={basePath}
|
|
155
|
-
/>
|
|
156
|
-
{children}
|
|
157
|
-
</Physics>
|
|
162
|
+
{physics ? (
|
|
163
|
+
<Physics debug={editMode} paused={editMode}>
|
|
164
|
+
{content}
|
|
165
|
+
</Physics>
|
|
166
|
+
) : content}
|
|
158
167
|
</GameCanvas>
|
|
159
168
|
|
|
160
169
|
<div style={toolbar.panel}>
|
|
@@ -11,7 +11,9 @@ import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./Instance
|
|
|
11
11
|
import { updateNode } from "./utils";
|
|
12
12
|
import { PhysicsProps } from "./components/PhysicsComponent";
|
|
13
13
|
import { EditorContext } from "./EditorContext";
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
// Dynamic type to avoid requiring @react-three/rapier when not using physics
|
|
16
|
+
type RapierRigidBody = any;
|
|
15
17
|
|
|
16
18
|
components.forEach(registerComponent);
|
|
17
19
|
|
|
@@ -19,7 +21,7 @@ const IDENTITY = new Matrix4();
|
|
|
19
21
|
|
|
20
22
|
export interface PrefabRootRef {
|
|
21
23
|
root: Group | null;
|
|
22
|
-
rigidBodyRefs: Map<string,
|
|
24
|
+
rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
@@ -56,7 +58,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
56
58
|
if (id === selectedId) setSelectedObject(obj);
|
|
57
59
|
}, [selectedId]);
|
|
58
60
|
|
|
59
|
-
const registerRigidBodyRef = useCallback((id: string, rb:
|
|
61
|
+
const registerRigidBodyRef = useCallback((id: string, rb: any) => {
|
|
60
62
|
rigidBodyRefs.current.set(id, rb);
|
|
61
63
|
}, []);
|
|
62
64
|
|
|
@@ -126,14 +128,20 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
126
128
|
texturesToLoad.forEach(file => {
|
|
127
129
|
if (textures[file] || loading.current.has(file)) return;
|
|
128
130
|
loading.current.add(file);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
|
|
132
|
+
// Handle full URLs (http/https) or regular paths
|
|
133
|
+
const path = file.startsWith("http://") || file.startsWith("https://")
|
|
134
|
+
? file
|
|
135
|
+
: file.startsWith("/")
|
|
131
136
|
? `${basePath}${file}`
|
|
132
137
|
: `${basePath}/${file}`;
|
|
133
138
|
|
|
134
139
|
loader.load(path, tex => {
|
|
135
140
|
tex.colorSpace = SRGBColorSpace;
|
|
136
141
|
setTextures(t => ({ ...t, [file]: tex }));
|
|
142
|
+
}, undefined, (err) => {
|
|
143
|
+
console.error(`Failed to load texture: ${path}`, err);
|
|
144
|
+
loading.current.delete(file);
|
|
137
145
|
});
|
|
138
146
|
});
|
|
139
147
|
}, [data, models, textures]);
|
|
@@ -434,7 +442,7 @@ interface RendererProps {
|
|
|
434
442
|
onSelect?: (id: string) => void;
|
|
435
443
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
436
444
|
registerRef: (id: string, obj: Object3D | null) => void;
|
|
437
|
-
registerRigidBodyRef: (id: string, rb:
|
|
445
|
+
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
438
446
|
loadedModels: Record<string, Object3D>;
|
|
439
447
|
loadedTextures: Record<string, Texture>;
|
|
440
448
|
editMode?: boolean;
|
|
@@ -55,20 +55,20 @@ function GeometryComponentEditor({
|
|
|
55
55
|
const currentArgs = values.args || currentSchema.defaults;
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap:
|
|
58
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
59
59
|
{currentSchema.labels.map((label, i) => (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
<Input
|
|
61
|
+
key={label}
|
|
62
|
+
label={label}
|
|
63
|
+
value={currentArgs[i] ?? currentSchema.defaults[i]}
|
|
64
|
+
step={0.1}
|
|
65
|
+
min={0.01}
|
|
66
|
+
onChange={value => {
|
|
67
|
+
const next = [...currentArgs];
|
|
68
|
+
next[i] = value;
|
|
69
|
+
onChangeMultiple({ args: next });
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
72
|
))}
|
|
73
73
|
</div>
|
|
74
74
|
);
|
|
@@ -67,7 +67,7 @@ export type FieldDefinition =
|
|
|
67
67
|
// Shared styles
|
|
68
68
|
const styles = {
|
|
69
69
|
input: {
|
|
70
|
-
width: '
|
|
70
|
+
width: '80px',
|
|
71
71
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
72
72
|
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
73
73
|
padding: '2px 4px',
|
|
@@ -75,11 +75,12 @@ const styles = {
|
|
|
75
75
|
color: 'rgba(165, 243, 252, 1)',
|
|
76
76
|
fontFamily: 'monospace',
|
|
77
77
|
outline: 'none',
|
|
78
|
+
textAlign: 'right',
|
|
78
79
|
} as React.CSSProperties,
|
|
79
80
|
label: {
|
|
80
81
|
display: 'block',
|
|
81
82
|
fontSize: '9px',
|
|
82
|
-
color: 'rgba(34, 211, 238, 0.
|
|
83
|
+
color: 'rgba(34, 211, 238, 0.9)',
|
|
83
84
|
textTransform: 'uppercase',
|
|
84
85
|
letterSpacing: '0.05em',
|
|
85
86
|
marginBottom: 2,
|
|
@@ -93,9 +94,10 @@ interface InputProps {
|
|
|
93
94
|
min?: number;
|
|
94
95
|
max?: number;
|
|
95
96
|
style?: React.CSSProperties;
|
|
97
|
+
label?: string;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
export function Input({ value, onChange, step, min, max, style }: InputProps) {
|
|
100
|
+
export function Input({ value, onChange, step, min, max, style, label }: InputProps) {
|
|
99
101
|
const [draft, setDraft] = useState<string>(() => value.toString());
|
|
100
102
|
|
|
101
103
|
useEffect(() => {
|
|
@@ -119,6 +121,93 @@ export function Input({ value, onChange, step, min, max, style }: InputProps) {
|
|
|
119
121
|
}
|
|
120
122
|
};
|
|
121
123
|
|
|
124
|
+
const dragState = useRef<{
|
|
125
|
+
startX: number;
|
|
126
|
+
startValue: number;
|
|
127
|
+
} | null>(null);
|
|
128
|
+
|
|
129
|
+
const startScrub = (e: React.PointerEvent) => {
|
|
130
|
+
if (!label) return;
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
|
|
133
|
+
dragState.current = {
|
|
134
|
+
startX: e.clientX,
|
|
135
|
+
startValue: value
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
139
|
+
document.body.style.cursor = "ew-resize";
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const onScrubMove = (e: React.PointerEvent) => {
|
|
143
|
+
if (!dragState.current) return;
|
|
144
|
+
|
|
145
|
+
const { startX, startValue } = dragState.current;
|
|
146
|
+
const dx = e.clientX - startX;
|
|
147
|
+
|
|
148
|
+
let speed = 0.02;
|
|
149
|
+
if (e.shiftKey) speed *= 0.1; // fine
|
|
150
|
+
if (e.altKey) speed *= 5; // coarse
|
|
151
|
+
|
|
152
|
+
let nextValue = startValue + dx * speed;
|
|
153
|
+
|
|
154
|
+
// Apply min/max constraints
|
|
155
|
+
if (min !== undefined && nextValue < min) nextValue = min;
|
|
156
|
+
if (max !== undefined && nextValue > max) nextValue = max;
|
|
157
|
+
|
|
158
|
+
setDraft(nextValue.toFixed(3));
|
|
159
|
+
onChange(nextValue);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const endScrub = (e: React.PointerEvent) => {
|
|
163
|
+
if (!dragState.current) return;
|
|
164
|
+
|
|
165
|
+
dragState.current = null;
|
|
166
|
+
document.body.style.cursor = "";
|
|
167
|
+
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (label) {
|
|
171
|
+
return (
|
|
172
|
+
<div style={{
|
|
173
|
+
display: 'flex',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'space-between',
|
|
176
|
+
}}>
|
|
177
|
+
<span
|
|
178
|
+
style={{
|
|
179
|
+
...styles.label,
|
|
180
|
+
marginBottom: 0,
|
|
181
|
+
cursor: 'ew-resize',
|
|
182
|
+
userSelect: 'none',
|
|
183
|
+
flex: '0 0 auto',
|
|
184
|
+
minWidth: 20,
|
|
185
|
+
}}
|
|
186
|
+
onPointerDown={startScrub}
|
|
187
|
+
onPointerMove={onScrubMove}
|
|
188
|
+
onPointerUp={endScrub}
|
|
189
|
+
>
|
|
190
|
+
{label}
|
|
191
|
+
</span>
|
|
192
|
+
<input
|
|
193
|
+
type="text"
|
|
194
|
+
value={draft}
|
|
195
|
+
onChange={handleChange}
|
|
196
|
+
onBlur={handleBlur}
|
|
197
|
+
onKeyDown={e => {
|
|
198
|
+
if (e.key === 'Enter') {
|
|
199
|
+
(e.target as HTMLInputElement).blur();
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
202
|
+
step={step}
|
|
203
|
+
min={min}
|
|
204
|
+
max={max}
|
|
205
|
+
style={{ ...styles.input, ...style }}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
122
211
|
return (
|
|
123
212
|
<input
|
|
124
213
|
type="text"
|
|
@@ -316,23 +405,25 @@ export function ColorInput({
|
|
|
316
405
|
return (
|
|
317
406
|
<div>
|
|
318
407
|
{label && <Label>{label}</Label>}
|
|
319
|
-
<div style={{ display: 'flex', gap:
|
|
408
|
+
<div style={{ display: 'flex', gap: 4, justifyContent: 'space-between' }}>
|
|
320
409
|
<input
|
|
321
410
|
type="color"
|
|
322
411
|
style={{
|
|
323
|
-
height:
|
|
324
|
-
width:
|
|
412
|
+
height: 32,
|
|
413
|
+
width: 48,
|
|
325
414
|
backgroundColor: 'transparent',
|
|
326
|
-
border: '
|
|
415
|
+
border: '1px solid rgba(34, 211, 238, 0.3)',
|
|
416
|
+
borderRadius: 4,
|
|
327
417
|
cursor: 'pointer',
|
|
328
418
|
padding: 0,
|
|
419
|
+
flexShrink: 0,
|
|
329
420
|
}}
|
|
330
421
|
value={value}
|
|
331
422
|
onChange={e => onChange(e.target.value)}
|
|
332
423
|
/>
|
|
333
424
|
<input
|
|
334
425
|
type="text"
|
|
335
|
-
style={{ ...styles.input,
|
|
426
|
+
style={{ ...styles.input, }}
|
|
336
427
|
value={value}
|
|
337
428
|
onChange={e => onChange(e.target.value)}
|
|
338
429
|
/>
|
|
@@ -376,7 +467,7 @@ export function BooleanInput({
|
|
|
376
467
|
onChange: (value: boolean) => void;
|
|
377
468
|
}) {
|
|
378
469
|
return (
|
|
379
|
-
<div>
|
|
470
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
380
471
|
{label && <Label>{label}</Label>}
|
|
381
472
|
<input
|
|
382
473
|
type="checkbox"
|
|
@@ -406,7 +497,7 @@ export function SelectInput({
|
|
|
406
497
|
options: { value: string; label: string }[];
|
|
407
498
|
}) {
|
|
408
499
|
return (
|
|
409
|
-
<div>
|
|
500
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
410
501
|
{label && <Label>{label}</Label>}
|
|
411
502
|
<select
|
|
412
503
|
style={styles.input as React.CSSProperties}
|
|
@@ -457,16 +548,15 @@ export function FieldRenderer({ fields, values, onChange }: FieldRendererProps)
|
|
|
457
548
|
|
|
458
549
|
case 'number':
|
|
459
550
|
return (
|
|
460
|
-
<
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
</div>
|
|
551
|
+
<Input
|
|
552
|
+
key={field.name}
|
|
553
|
+
label={field.label}
|
|
554
|
+
value={value ?? 0}
|
|
555
|
+
onChange={v => updateField(field.name, v)}
|
|
556
|
+
min={field.min}
|
|
557
|
+
max={field.max}
|
|
558
|
+
step={field.step}
|
|
559
|
+
/>
|
|
470
560
|
);
|
|
471
561
|
|
|
472
562
|
case 'string':
|
|
@@ -55,7 +55,15 @@ function TexturePicker({
|
|
|
55
55
|
onClick={() => setShowPicker(!showPicker)}
|
|
56
56
|
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
57
57
|
>
|
|
58
|
-
{showPicker ? '
|
|
58
|
+
{showPicker ? 'Cancel' : 'Change'}
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => {
|
|
62
|
+
onChange(undefined as any);
|
|
63
|
+
}}
|
|
64
|
+
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }}
|
|
65
|
+
>
|
|
66
|
+
Clear
|
|
59
67
|
</button>
|
|
60
68
|
{showPicker && (
|
|
61
69
|
<div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
|
|
@@ -106,12 +114,20 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
106
114
|
render: ({ value, onChange }: { value: [number, number] | undefined; onChange: (v: [number, number]) => void }) => (
|
|
107
115
|
<div style={{ display: 'flex', gap: 2 }}>
|
|
108
116
|
<Input
|
|
117
|
+
label="X"
|
|
109
118
|
value={value?.[0] ?? 1}
|
|
110
119
|
onChange={v => onChange([v, value?.[1] ?? 1])}
|
|
120
|
+
min={0.01}
|
|
121
|
+
max={100}
|
|
122
|
+
step={0.1}
|
|
111
123
|
/>
|
|
112
124
|
<Input
|
|
125
|
+
label="Y"
|
|
113
126
|
value={value?.[1] ?? 1}
|
|
114
127
|
onChange={v => onChange([value?.[0] ?? 1, v])}
|
|
128
|
+
min={0.01}
|
|
129
|
+
max={100}
|
|
130
|
+
step={0.1}
|
|
115
131
|
/>
|
|
116
132
|
</div>
|
|
117
133
|
),
|
|
@@ -162,15 +178,15 @@ function MaterialComponentView({ properties, loadedTextures }: { properties: Mat
|
|
|
162
178
|
const texture = textureName && loadedTextures ? loadedTextures[textureName] : undefined;
|
|
163
179
|
|
|
164
180
|
// Destructure all material props and separate custom texture handling props
|
|
165
|
-
const {
|
|
166
|
-
texture: _texture,
|
|
167
|
-
repeat: _repeat,
|
|
168
|
-
repeatCount: _repeatCount,
|
|
181
|
+
const {
|
|
182
|
+
texture: _texture,
|
|
183
|
+
repeat: _repeat,
|
|
184
|
+
repeatCount: _repeatCount,
|
|
169
185
|
generateMipmaps: _generateMipmaps,
|
|
170
186
|
minFilter: _minFilter,
|
|
171
187
|
magFilter: _magFilter,
|
|
172
188
|
map: _map, // Filter out map since we set it explicitly
|
|
173
|
-
...materialProps
|
|
189
|
+
...materialProps
|
|
174
190
|
} = properties || {};
|
|
175
191
|
|
|
176
192
|
const minFilterMap: Record<string, MinificationTextureFilter> = {
|
|
@@ -39,7 +39,15 @@ function ModelPicker({
|
|
|
39
39
|
onClick={() => setShowPicker(!showPicker)}
|
|
40
40
|
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
41
41
|
>
|
|
42
|
-
{showPicker ? '
|
|
42
|
+
{showPicker ? 'Cancel' : 'Change'}
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => {
|
|
46
|
+
onChange(undefined as any);
|
|
47
|
+
}}
|
|
48
|
+
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4, marginLeft: 4 }}
|
|
49
|
+
>
|
|
50
|
+
Clear
|
|
43
51
|
</button>
|
|
44
52
|
{showPicker && (
|
|
45
53
|
<div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
|
|
@@ -115,7 +115,15 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
|
|
|
115
115
|
const { type, colliders, sensor, activeCollisionTypes, ...otherProps } = properties;
|
|
116
116
|
const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
|
|
117
117
|
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
|
118
|
-
|
|
118
|
+
|
|
119
|
+
// Try to get rapier context - will be null if not inside <Physics>
|
|
120
|
+
let rapier: any = null;
|
|
121
|
+
try {
|
|
122
|
+
const rapierContext = useRapier();
|
|
123
|
+
rapier = rapierContext.rapier;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Not inside Physics context - that's ok, just won't have rapier features
|
|
126
|
+
}
|
|
119
127
|
|
|
120
128
|
// Register RigidBody ref when it's available
|
|
121
129
|
useEffect(() => {
|
|
@@ -131,7 +139,7 @@ function PhysicsComponentView({ properties, children, position, rotation, scale,
|
|
|
131
139
|
|
|
132
140
|
// Configure active collision types for kinematic/sensor bodies
|
|
133
141
|
useEffect(() => {
|
|
134
|
-
if (activeCollisionTypes === 'all' && rigidBodyRef.current) {
|
|
142
|
+
if (activeCollisionTypes === 'all' && rigidBodyRef.current && rapier) {
|
|
135
143
|
const rb = rigidBodyRef.current;
|
|
136
144
|
// Apply to all colliders on this rigid body
|
|
137
145
|
for (let i = 0; i < rb.numColliders(); i++) {
|
|
@@ -124,7 +124,9 @@ export const tree = {
|
|
|
124
124
|
scroll: {
|
|
125
125
|
overflowY: 'auto' as const,
|
|
126
126
|
padding: 4,
|
|
127
|
-
|
|
127
|
+
scrollbarWidth: 'thin' as const,
|
|
128
|
+
scrollbarColor: 'rgba(255,255,255,0.06) transparent',
|
|
129
|
+
} as React.CSSProperties,
|
|
128
130
|
row: {
|
|
129
131
|
display: 'flex',
|
|
130
132
|
alignItems: 'center',
|