react-three-game 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/assets/architecture.png +0 -0
- package/dist/tools/prefabeditor/EditorTree.js +30 -36
- package/dist/tools/prefabeditor/EditorUI.js +2 -1
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +1 -1
- package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
- package/dist/tools/prefabeditor/components/MaterialComponent.js +2 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +3 -3
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
- package/dist/tools/prefabeditor/components/TransformComponent.js +64 -9
- package/dist/tools/prefabeditor/types.d.ts +1 -0
- package/dist/tools/prefabeditor/utils.d.ts +1 -0
- package/dist/tools/prefabeditor/utils.js +20 -2
- package/package.json +1 -1
- package/src/tools/prefabeditor/EditorTree.tsx +49 -40
- package/src/tools/prefabeditor/EditorUI.tsx +4 -2
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +35 -35
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +2 -2
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +19 -19
- package/src/tools/prefabeditor/components/ModelComponent.tsx +6 -6
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +6 -6
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +15 -15
- package/src/tools/prefabeditor/components/TransformComponent.tsx +125 -30
- package/src/tools/prefabeditor/types.ts +1 -0
- package/src/tools/prefabeditor/utils.ts +30 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|

|
|
10
|
+

|
|
10
11
|
|
|
11
12
|
## Usage
|
|
12
13
|
|
|
@@ -132,3 +133,20 @@ npm run release # build + publish
|
|
|
132
133
|
---
|
|
133
134
|
|
|
134
135
|
React 19 · Three.js WebGPU · TypeScript 5 · Rapier WASM · MIT License
|
|
136
|
+
|
|
137
|
+
## Manifest generation script
|
|
138
|
+
|
|
139
|
+
A small helper script is included to auto-generate asset manifests from the `public` folder. See `docs/generate-manifests.sh`.
|
|
140
|
+
|
|
141
|
+
- What it does: searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to `public/models/manifest.json`, `public/textures/manifest.json`, and `public/sound/manifest.json`. These manifest files are used top populate the Asset Viewer in the the Editor.
|
|
142
|
+
- How to run:
|
|
143
|
+
|
|
144
|
+
1. Make it executable (once):
|
|
145
|
+
|
|
146
|
+
chmod +x docs/generate-manifests.sh
|
|
147
|
+
|
|
148
|
+
2. Run the script from the repo root (zsh/bash):
|
|
149
|
+
|
|
150
|
+
./docs/generate-manifests.sh
|
|
151
|
+
|
|
152
|
+
The script is intentionally simple and portable (uses `find`/`sed`). If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
|
|
Binary file
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { getComponent } from './components/ComponentRegistry';
|
|
4
4
|
import { base, tree, menu } from './styles';
|
|
5
|
-
import { findNode, findParent, deleteNode, cloneNode } from './utils';
|
|
5
|
+
import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
|
|
6
6
|
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }) {
|
|
7
7
|
const [contextMenu, setContextMenu] = useState(null);
|
|
8
8
|
const [draggedId, setDraggedId] = useState(null);
|
|
@@ -28,6 +28,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
28
28
|
var _a;
|
|
29
29
|
const newNode = {
|
|
30
30
|
id: crypto.randomUUID(),
|
|
31
|
+
name: "New Node",
|
|
31
32
|
components: {
|
|
32
33
|
transform: {
|
|
33
34
|
type: "Transform",
|
|
@@ -35,30 +36,25 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
};
|
|
38
|
-
setPrefabData(prev => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
parent.children = parent.children || [];
|
|
43
|
-
parent.children.push(newNode);
|
|
44
|
-
}
|
|
45
|
-
return Object.assign(Object.assign({}, prev), { root: newRoot });
|
|
46
|
-
});
|
|
39
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parentId, parent => {
|
|
40
|
+
var _a;
|
|
41
|
+
return (Object.assign(Object.assign({}, parent), { children: [...((_a = parent.children) !== null && _a !== void 0 ? _a : []), newNode] }));
|
|
42
|
+
}) })));
|
|
47
43
|
setContextMenu(null);
|
|
48
44
|
};
|
|
49
45
|
const handleDuplicate = (nodeId) => {
|
|
50
46
|
if (nodeId === prefabData.root.id)
|
|
51
47
|
return;
|
|
52
48
|
setPrefabData(prev => {
|
|
53
|
-
const
|
|
54
|
-
const parent = findParent(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
const node = findNode(prev.root, nodeId);
|
|
50
|
+
const parent = findParent(prev.root, nodeId);
|
|
51
|
+
if (!node || !parent)
|
|
52
|
+
return prev;
|
|
53
|
+
const clone = cloneNode(node);
|
|
54
|
+
return Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, parent.id, p => {
|
|
55
|
+
var _a;
|
|
56
|
+
return (Object.assign(Object.assign({}, p), { children: [...((_a = p.children) !== null && _a !== void 0 ? _a : []), clone] }));
|
|
57
|
+
}) });
|
|
62
58
|
});
|
|
63
59
|
setContextMenu(null);
|
|
64
60
|
};
|
|
@@ -92,28 +88,26 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
92
88
|
return;
|
|
93
89
|
e.preventDefault();
|
|
94
90
|
setPrefabData(prev => {
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (draggedNode && findNode(draggedNode, targetId))
|
|
99
|
-
return prev;
|
|
100
|
-
const parent = findParent(newRoot, draggedId);
|
|
101
|
-
if (!parent)
|
|
91
|
+
const draggedNode = findNode(prev.root, draggedId);
|
|
92
|
+
const oldParent = findParent(prev.root, draggedId);
|
|
93
|
+
if (!draggedNode || !oldParent)
|
|
102
94
|
return prev;
|
|
103
|
-
|
|
104
|
-
if (
|
|
95
|
+
// Prevent dropping into own subtree
|
|
96
|
+
if (findNode(draggedNode, targetId))
|
|
105
97
|
return prev;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
// 1. Remove from old parent
|
|
99
|
+
let root = updateNodeById(prev.root, oldParent.id, p => (Object.assign(Object.assign({}, p), { children: p.children.filter(c => c.id !== draggedId) })));
|
|
100
|
+
// 2. Add to new parent
|
|
101
|
+
root = updateNodeById(root, targetId, t => {
|
|
102
|
+
var _a;
|
|
103
|
+
return (Object.assign(Object.assign({}, t), { children: [...((_a = t.children) !== null && _a !== void 0 ? _a : []), draggedNode] }));
|
|
104
|
+
});
|
|
105
|
+
return Object.assign(Object.assign({}, prev), { root });
|
|
113
106
|
});
|
|
114
107
|
setDraggedId(null);
|
|
115
108
|
};
|
|
116
109
|
const renderNode = (node, depth = 0) => {
|
|
110
|
+
var _a;
|
|
117
111
|
if (!node)
|
|
118
112
|
return null;
|
|
119
113
|
const isSelected = node.id === selectedId;
|
|
@@ -126,7 +120,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
126
120
|
marginRight: 4,
|
|
127
121
|
cursor: 'pointer',
|
|
128
122
|
visibility: hasChildren ? 'visible' : 'hidden'
|
|
129
|
-
}, 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: node.id })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
123
|
+
}, 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 })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
130
124
|
};
|
|
131
125
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => setContextMenu(null), children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Scene" }), _jsx("span", { children: collapsed ? '▶' : '◀' })] }), !collapsed && _jsx("div", { 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" })] }))] }))] }));
|
|
132
126
|
}
|
|
@@ -32,6 +32,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
32
32
|
return _jsxs(_Fragment, { children: [_jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
|
|
33
33
|
}
|
|
34
34
|
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
|
|
35
|
+
var _a;
|
|
35
36
|
const ALL_COMPONENTS = getAllComponents();
|
|
36
37
|
const allKeys = Object.keys(ALL_COMPONENTS);
|
|
37
38
|
const available = allKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); });
|
|
@@ -41,7 +42,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
41
42
|
if (!newAvailable.includes(addType))
|
|
42
43
|
setAddType(newAvailable[0] || "");
|
|
43
44
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
44
|
-
return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: node.
|
|
45
|
+
return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
45
46
|
if (!comp)
|
|
46
47
|
return null;
|
|
47
48
|
const def = ALL_COMPONENTS[comp.type];
|
|
@@ -17,7 +17,7 @@ function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
|
17
17
|
shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
|
|
18
18
|
targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
|
|
19
19
|
};
|
|
20
|
-
return _jsxs("div", {
|
|
20
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Near" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Far" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Top" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Bottom" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Left" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Right" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "X" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Y" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Z" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
|
|
21
21
|
}
|
|
22
22
|
function DirectionalLightView({ properties, editMode }) {
|
|
23
23
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
function GeometryComponentEditor({ component, onUpdate }) {
|
|
3
|
-
return _jsxs("div", { children: [_jsx("label", {
|
|
3
|
+
return _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Type" }), _jsxs("select", { style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.geometryType, onChange: e => onUpdate({ geometryType: e.target.value }), children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] });
|
|
4
4
|
}
|
|
5
5
|
// View for Geometry component
|
|
6
6
|
function GeometryComponentView({ properties, children }) {
|
|
@@ -11,11 +11,11 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
11
11
|
.then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
|
|
12
12
|
.catch(console.error);
|
|
13
13
|
}, [basePath]);
|
|
14
|
-
return (_jsxs("div", {
|
|
14
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Texture" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 4, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Repeat (X, Y)" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
|
|
15
15
|
var _a, _b;
|
|
16
16
|
const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
|
|
17
17
|
onUpdate({ 'repeatCount': [parseFloat(e.target.value), y] });
|
|
18
|
-
} }), _jsx("input", { type: "number",
|
|
18
|
+
} }), _jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_d = (_c = component.properties.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1, onChange: e => {
|
|
19
19
|
var _a, _b;
|
|
20
20
|
const x = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1;
|
|
21
21
|
onUpdate({ 'repeatCount': [x, parseFloat(e.target.value)] });
|
|
@@ -15,7 +15,7 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
15
15
|
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
16
16
|
onUpdate({ 'filename': filename });
|
|
17
17
|
};
|
|
18
|
-
return _jsxs("div", { children: [_jsxs("div", {
|
|
18
|
+
return _jsxs("div", { children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Model" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
|
|
19
19
|
}
|
|
20
20
|
// View for Model component
|
|
21
21
|
function ModelComponentView({ properties, loadedModels, children }) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { RigidBody } from "@react-three/rapier";
|
|
3
|
-
const selectClass =
|
|
4
|
-
const labelClass =
|
|
3
|
+
const selectClass = { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' };
|
|
4
|
+
const labelClass = { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 };
|
|
5
5
|
function PhysicsComponentEditor({ component, onUpdate }) {
|
|
6
6
|
const { type = 'dynamic', collider = 'hull' } = component.properties;
|
|
7
|
-
return (_jsxs("div", { children: [_jsx("label", {
|
|
7
|
+
return (_jsxs("div", { children: [_jsx("label", { style: labelClass, children: "Type" }), _jsxs("select", { style: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { style: Object.assign(Object.assign({}, labelClass), { marginTop: 8 }), children: "Collider" }), _jsxs("select", { style: selectClass, 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)" })] })] }));
|
|
8
8
|
}
|
|
9
9
|
function PhysicsComponentView({ properties, editMode, children }) {
|
|
10
10
|
if (editMode)
|
|
@@ -10,7 +10,7 @@ function SpotLightComponentEditor({ component, onUpdate }) {
|
|
|
10
10
|
distance: (_e = component.properties.distance) !== null && _e !== void 0 ? _e : 100,
|
|
11
11
|
castShadow: (_f = component.properties.castShadow) !== null && _f !== void 0 ? _f : true
|
|
12
12
|
};
|
|
13
|
-
return _jsxs("div", {
|
|
13
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
|
|
14
14
|
}
|
|
15
15
|
function SpotLightView({ properties, editMode }) {
|
|
16
16
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
|
|
3
4
|
const s = {
|
|
4
5
|
button: {
|
|
@@ -14,7 +15,7 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
|
|
|
14
15
|
background: 'rgba(255,255,255,0.10)',
|
|
15
16
|
},
|
|
16
17
|
};
|
|
17
|
-
return _jsxs("div", {
|
|
18
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : {})), onPointerEnter: (e) => {
|
|
18
19
|
if (transformMode !== mode)
|
|
19
20
|
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
20
21
|
}, onPointerLeave: (e) => {
|
|
@@ -33,15 +34,69 @@ const TransformComponent = {
|
|
|
33
34
|
};
|
|
34
35
|
export default TransformComponent;
|
|
35
36
|
export function Vector3Input({ label, value, onChange }) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const [draft, setDraft] = useState(() => value.map(v => v.toString()));
|
|
38
|
+
// Sync external changes (gizmo, undo, etc.)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setDraft(value.map(v => v.toString()));
|
|
41
|
+
}, [value[0], value[1], value[2]]);
|
|
42
|
+
const dragState = useRef(null);
|
|
43
|
+
const commit = (index) => {
|
|
44
|
+
const num = parseFloat(draft[index]);
|
|
45
|
+
if (Number.isFinite(num)) {
|
|
46
|
+
const next = [...value];
|
|
47
|
+
next[index] = num;
|
|
48
|
+
onChange(next);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const startScrub = (e, index) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
dragState.current = {
|
|
54
|
+
index,
|
|
55
|
+
startX: e.clientX,
|
|
56
|
+
startValue: value[index]
|
|
57
|
+
};
|
|
58
|
+
e.target.setPointerCapture(e.pointerId);
|
|
59
|
+
document.body.style.cursor = "ew-resize";
|
|
60
|
+
};
|
|
61
|
+
const onScrubMove = (e) => {
|
|
62
|
+
if (!dragState.current)
|
|
63
|
+
return;
|
|
64
|
+
const { index, startX, startValue } = dragState.current;
|
|
65
|
+
const dx = e.clientX - startX;
|
|
66
|
+
let speed = 0.02;
|
|
67
|
+
if (e.shiftKey)
|
|
68
|
+
speed *= 0.1; // fine
|
|
69
|
+
if (e.altKey)
|
|
70
|
+
speed *= 5; // coarse
|
|
71
|
+
const nextValue = startValue + dx * speed;
|
|
72
|
+
const next = [...value];
|
|
73
|
+
next[index] = nextValue;
|
|
74
|
+
setDraft(d => {
|
|
75
|
+
const copy = [...d];
|
|
76
|
+
copy[index] = nextValue.toFixed(3);
|
|
77
|
+
return copy;
|
|
78
|
+
});
|
|
79
|
+
onChange(next);
|
|
80
|
+
};
|
|
81
|
+
const endScrub = (e) => {
|
|
82
|
+
if (!dragState.current)
|
|
83
|
+
return;
|
|
84
|
+
dragState.current = null;
|
|
85
|
+
document.body.style.cursor = "";
|
|
86
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
40
87
|
};
|
|
41
88
|
const axes = [
|
|
42
|
-
{ key:
|
|
43
|
-
{ key:
|
|
44
|
-
{ key:
|
|
89
|
+
{ key: "x", color: "red", index: 0 },
|
|
90
|
+
{ key: "y", color: "green", index: 1 },
|
|
91
|
+
{ key: "z", color: "blue", index: 2 }
|
|
45
92
|
];
|
|
46
|
-
return _jsxs("div", {
|
|
93
|
+
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: label }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: 4, backgroundColor: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(34, 211, 238, 0.2)', borderRadius: 4, padding: '4px 6px', minHeight: 32 }, children: [_jsx("span", { style: { fontSize: '12px', fontWeight: 'bold', color: color === 'red' ? 'rgba(248, 113, 113, 1)' : color === 'green' ? 'rgba(134, 239, 172, 1)' : 'rgba(96, 165, 250, 1)', width: 12, cursor: 'ew-resize', userSelect: 'none' }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: { flex: 1, backgroundColor: 'transparent', fontSize: '12px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none', width: '100%', minWidth: 0 }, type: "text", value: draft[index], onChange: e => {
|
|
94
|
+
const next = [...draft];
|
|
95
|
+
next[index] = e.target.value;
|
|
96
|
+
setDraft(next);
|
|
97
|
+
}, onBlur: () => commit(index), onKeyDown: e => {
|
|
98
|
+
if (e.key === "Enter") {
|
|
99
|
+
e.target.blur();
|
|
100
|
+
}
|
|
101
|
+
} })] }, key))) })] }));
|
|
47
102
|
}
|
|
@@ -17,3 +17,4 @@ export declare function deleteNode(root: GameObject, id: string): GameObject | n
|
|
|
17
17
|
export declare function cloneNode(node: GameObject): GameObject;
|
|
18
18
|
/** Get component data from a node */
|
|
19
19
|
export declare function getComponent<T = any>(node: GameObject, type: string): T | undefined;
|
|
20
|
+
export declare function updateNodeById(root: GameObject, id: string, updater: (node: GameObject) => GameObject): GameObject;
|
|
@@ -61,8 +61,8 @@ export function deleteNode(root, id) {
|
|
|
61
61
|
}
|
|
62
62
|
/** Deep clone a node with new IDs */
|
|
63
63
|
export function cloneNode(node) {
|
|
64
|
-
var _a;
|
|
65
|
-
return Object.assign(Object.assign({}, node), { id: crypto.randomUUID(),
|
|
64
|
+
var _a, _b;
|
|
65
|
+
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
66
|
}
|
|
67
67
|
/** Get component data from a node */
|
|
68
68
|
export function getComponent(node, type) {
|
|
@@ -70,3 +70,21 @@ export function getComponent(node, type) {
|
|
|
70
70
|
const comp = Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).find(c => (c === null || c === void 0 ? void 0 : c.type) === type);
|
|
71
71
|
return comp === null || comp === void 0 ? void 0 : comp.properties;
|
|
72
72
|
}
|
|
73
|
+
export function updateNodeById(root, id, updater) {
|
|
74
|
+
if (root.id === id) {
|
|
75
|
+
return updater(root);
|
|
76
|
+
}
|
|
77
|
+
if (!root.children) {
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
let didChange = false;
|
|
81
|
+
const newChildren = root.children.map(child => {
|
|
82
|
+
const updated = updateNodeById(child, id, updater);
|
|
83
|
+
if (updated !== child)
|
|
84
|
+
didChange = true;
|
|
85
|
+
return updated;
|
|
86
|
+
});
|
|
87
|
+
if (!didChange)
|
|
88
|
+
return root;
|
|
89
|
+
return Object.assign(Object.assign({}, root), { children: newChildren });
|
|
90
|
+
}
|