react-three-game 0.0.15 → 0.0.17
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 +132 -171
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/tools/dragdrop/DragDropLoader.js +1 -0
- package/dist/tools/prefabeditor/EditorTree.js +141 -15
- package/dist/tools/prefabeditor/EditorUI.js +154 -6
- package/dist/tools/prefabeditor/PrefabEditor.js +128 -5
- package/dist/tools/prefabeditor/PrefabRoot.js +34 -15
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +2 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/RotatorComponent.js +42 -0
- package/dist/tools/prefabeditor/components/TransformComponent.js +28 -3
- package/dist/tools/prefabeditor/types.d.ts +2 -2
- package/dist/tools/prefabeditor/types.js +1 -0
- package/package.json +7 -7
- package/src/index.ts +4 -0
- package/src/tools/dragdrop/DragDropLoader.tsx +1 -0
- package/src/tools/prefabeditor/EditorTree.tsx +193 -30
- package/src/tools/prefabeditor/EditorUI.tsx +185 -63
- package/src/tools/prefabeditor/PrefabEditor.tsx +202 -24
- package/src/tools/prefabeditor/PrefabRoot.tsx +38 -19
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +7 -1
- package/src/tools/prefabeditor/components/TransformComponent.tsx +69 -16
- package/src/tools/prefabeditor/types.ts +3 -3
|
@@ -4,6 +4,46 @@ import EditorTree from './EditorTree';
|
|
|
4
4
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
5
5
|
function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
|
|
6
6
|
const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
|
|
7
|
+
const ui = {
|
|
8
|
+
panel: {
|
|
9
|
+
position: 'absolute',
|
|
10
|
+
top: 8,
|
|
11
|
+
right: 8,
|
|
12
|
+
zIndex: 20,
|
|
13
|
+
width: 260,
|
|
14
|
+
background: 'rgba(0,0,0,0.55)',
|
|
15
|
+
color: 'rgba(255,255,255,0.9)',
|
|
16
|
+
border: '1px solid rgba(255,255,255,0.12)',
|
|
17
|
+
borderRadius: 6,
|
|
18
|
+
overflow: 'hidden',
|
|
19
|
+
backdropFilter: 'blur(6px)',
|
|
20
|
+
WebkitBackdropFilter: 'blur(6px)',
|
|
21
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
22
|
+
fontSize: 11,
|
|
23
|
+
lineHeight: 1.2,
|
|
24
|
+
},
|
|
25
|
+
header: {
|
|
26
|
+
padding: '4px 6px',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'space-between',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
background: 'rgba(255,255,255,0.05)',
|
|
32
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
33
|
+
textTransform: 'uppercase',
|
|
34
|
+
letterSpacing: '0.08em',
|
|
35
|
+
fontSize: 10,
|
|
36
|
+
color: 'rgba(255,255,255,0.7)',
|
|
37
|
+
userSelect: 'none',
|
|
38
|
+
WebkitUserSelect: 'none',
|
|
39
|
+
},
|
|
40
|
+
left: {
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
top: 8,
|
|
43
|
+
left: 8,
|
|
44
|
+
zIndex: 20,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
7
47
|
const updateNode = (updater) => {
|
|
8
48
|
if (!prefabData || !setPrefabData || !selectedId)
|
|
9
49
|
return;
|
|
@@ -24,12 +64,112 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
24
64
|
};
|
|
25
65
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
26
66
|
// if (!selectedNode) return null;
|
|
27
|
-
return _jsxs(_Fragment, { children: [_jsxs("div", { style:
|
|
67
|
+
return _jsxs(_Fragment, { children: [_jsxs("div", { style: ui.panel, children: [_jsxs("div", { style: ui.header, onClick: () => setIsInspectorCollapsed(!isInspectorCollapsed), onPointerEnter: (e) => {
|
|
68
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
69
|
+
}, onPointerLeave: (e) => {
|
|
70
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.05)';
|
|
71
|
+
}, children: [_jsx("span", { children: "Inspector" }), _jsx("span", { style: { fontSize: 10, opacity: 0.8 }, children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: ui.left, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
|
|
28
72
|
}
|
|
29
73
|
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
|
|
30
74
|
const ALL_COMPONENTS = getAllComponents();
|
|
31
75
|
const allComponentKeys = Object.keys(ALL_COMPONENTS);
|
|
32
76
|
const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
|
|
77
|
+
const s = {
|
|
78
|
+
root: {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
flexDirection: 'column',
|
|
81
|
+
gap: 6,
|
|
82
|
+
padding: 6,
|
|
83
|
+
maxHeight: '80vh',
|
|
84
|
+
overflowY: 'auto',
|
|
85
|
+
},
|
|
86
|
+
section: {
|
|
87
|
+
paddingBottom: 6,
|
|
88
|
+
borderBottom: '1px solid rgba(255,255,255,0.10)',
|
|
89
|
+
},
|
|
90
|
+
label: {
|
|
91
|
+
display: 'block',
|
|
92
|
+
fontSize: 10,
|
|
93
|
+
opacity: 0.7,
|
|
94
|
+
textTransform: 'uppercase',
|
|
95
|
+
letterSpacing: '0.08em',
|
|
96
|
+
marginBottom: 4,
|
|
97
|
+
},
|
|
98
|
+
input: {
|
|
99
|
+
width: '100%',
|
|
100
|
+
background: 'rgba(255,255,255,0.06)',
|
|
101
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
102
|
+
borderRadius: 4,
|
|
103
|
+
padding: '4px 6px',
|
|
104
|
+
color: 'rgba(255,255,255,0.92)',
|
|
105
|
+
font: 'inherit',
|
|
106
|
+
outline: 'none',
|
|
107
|
+
},
|
|
108
|
+
row: {
|
|
109
|
+
display: 'flex',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
justifyContent: 'space-between',
|
|
112
|
+
gap: 8,
|
|
113
|
+
},
|
|
114
|
+
button: {
|
|
115
|
+
padding: '2px 6px',
|
|
116
|
+
background: 'transparent',
|
|
117
|
+
color: 'rgba(255,255,255,0.9)',
|
|
118
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
119
|
+
borderRadius: 4,
|
|
120
|
+
cursor: 'pointer',
|
|
121
|
+
font: 'inherit',
|
|
122
|
+
},
|
|
123
|
+
buttonActive: {
|
|
124
|
+
background: 'rgba(255,255,255,0.10)',
|
|
125
|
+
},
|
|
126
|
+
smallDanger: {
|
|
127
|
+
background: 'transparent',
|
|
128
|
+
border: 'none',
|
|
129
|
+
cursor: 'pointer',
|
|
130
|
+
color: 'rgba(255,120,120,0.95)',
|
|
131
|
+
font: 'inherit',
|
|
132
|
+
padding: '2px 4px',
|
|
133
|
+
},
|
|
134
|
+
componentHeader: {
|
|
135
|
+
display: 'flex',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'space-between',
|
|
138
|
+
padding: '4px 0',
|
|
139
|
+
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
140
|
+
marginBottom: 4,
|
|
141
|
+
},
|
|
142
|
+
componentTitle: {
|
|
143
|
+
fontSize: 10,
|
|
144
|
+
textTransform: 'uppercase',
|
|
145
|
+
letterSpacing: '0.08em',
|
|
146
|
+
opacity: 0.8,
|
|
147
|
+
},
|
|
148
|
+
select: {
|
|
149
|
+
flex: 1,
|
|
150
|
+
background: 'rgba(255,255,255,0.06)',
|
|
151
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
152
|
+
borderRadius: 4,
|
|
153
|
+
padding: '4px 6px',
|
|
154
|
+
color: 'rgba(255,255,255,0.92)',
|
|
155
|
+
font: 'inherit',
|
|
156
|
+
outline: 'none',
|
|
157
|
+
},
|
|
158
|
+
addButton: {
|
|
159
|
+
width: 28,
|
|
160
|
+
padding: '4px 0',
|
|
161
|
+
background: 'rgba(255,255,255,0.08)',
|
|
162
|
+
color: 'rgba(255,255,255,0.92)',
|
|
163
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
164
|
+
borderRadius: 4,
|
|
165
|
+
cursor: 'pointer',
|
|
166
|
+
font: 'inherit',
|
|
167
|
+
},
|
|
168
|
+
disabled: {
|
|
169
|
+
opacity: 0.35,
|
|
170
|
+
cursor: 'not-allowed',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
33
173
|
const componentKeys = Object.keys(node.components || {}).join(',');
|
|
34
174
|
useEffect(() => {
|
|
35
175
|
// Components stored on nodes use lowercase keys (e.g. 'geometry'),
|
|
@@ -39,19 +179,19 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
39
179
|
setAddComponentType(available[0] || "");
|
|
40
180
|
}
|
|
41
181
|
}, [componentKeys, addComponentType, node.components, allComponentKeys]);
|
|
42
|
-
return _jsxs("div", {
|
|
182
|
+
return _jsxs("div", { style: s.root, children: [_jsx("div", { style: s.section, children: _jsx("input", { style: s.input, value: node.id, onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { id: e.target.value }))) }) }), _jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, s.row), s.section), { paddingBottom: 6 }), children: [_jsx("label", { style: Object.assign(Object.assign({}, s.label), { marginBottom: 0 }), children: "Components" }), _jsx("button", { onClick: deleteNode, style: s.smallDanger, title: "Delete node", children: "\u2715" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
43
183
|
if (!comp)
|
|
44
184
|
return null;
|
|
45
185
|
const componentDef = ALL_COMPONENTS[comp.type];
|
|
46
186
|
if (!componentDef)
|
|
47
|
-
return _jsxs("div", {
|
|
187
|
+
return _jsxs("div", { style: { padding: '4px 0', color: 'rgba(255,120,120,0.95)', fontSize: 11 }, children: ["Unknown component type: ", comp.type, _jsx("textarea", { defaultValue: JSON.stringify(comp) })] }, key);
|
|
48
188
|
const EditorComp = componentDef.Editor;
|
|
49
|
-
return (_jsxs("div", {
|
|
189
|
+
return (_jsxs("div", { style: { padding: '0 2px' }, children: [_jsxs("div", { style: s.componentHeader, children: [_jsx("span", { style: s.componentTitle, children: key }), _jsx("button", { onClick: () => updateNode(n => {
|
|
50
190
|
const components = Object.assign({}, n.components);
|
|
51
191
|
delete components[key];
|
|
52
192
|
return Object.assign(Object.assign({}, n), { components });
|
|
53
|
-
}),
|
|
54
|
-
}), _jsxs("div", {
|
|
193
|
+
}), style: s.smallDanger, title: "Remove component", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, 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, transformMode: transformMode, setTransformMode: setTransformMode })) : null] }, key));
|
|
194
|
+
}), _jsxs("div", { style: Object.assign(Object.assign({}, s.section), { borderBottom: 'none', paddingBottom: 0 }), children: [_jsx("label", { style: s.label, children: "Add Component" }), _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx("select", { style: s.select, value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { style: Object.assign(Object.assign({}, s.addButton), (!addComponentType ? s.disabled : null)), disabled: !addComponentType, onClick: () => {
|
|
55
195
|
var _a;
|
|
56
196
|
if (!addComponentType)
|
|
57
197
|
return;
|
|
@@ -60,6 +200,14 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
60
200
|
const key = addComponentType.toLowerCase();
|
|
61
201
|
updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: { type: def.name, properties: def.defaultProperties } }) })));
|
|
62
202
|
}
|
|
203
|
+
}, onPointerEnter: (e) => {
|
|
204
|
+
if (!addComponentType)
|
|
205
|
+
return;
|
|
206
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.12)';
|
|
207
|
+
}, onPointerLeave: (e) => {
|
|
208
|
+
if (!addComponentType)
|
|
209
|
+
return;
|
|
210
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
63
211
|
}, children: "+" })] })] })] });
|
|
64
212
|
}
|
|
65
213
|
function findNode(root, id) {
|
|
@@ -50,11 +50,134 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
|
|
|
50
50
|
};
|
|
51
51
|
return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
|
|
52
52
|
// props for edit mode
|
|
53
|
-
editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsx(SaveDataPanel, { currentData: loadedPrefab, onDataChange: updatePrefab, editMode: editMode, onEditModeChange: setEditMode }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
|
|
54
|
+
};
|
|
55
|
+
const SaveDataPanel = ({ currentData, onDataChange, editMode, onEditModeChange }) => {
|
|
56
|
+
const [history, setHistory] = useState([currentData]);
|
|
57
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
58
|
+
const throttleTimeoutRef = useRef(null);
|
|
59
|
+
const lastSavedDataRef = useRef(JSON.stringify(currentData));
|
|
60
|
+
// Define undo/redo handlers
|
|
61
|
+
const handleUndo = () => {
|
|
62
|
+
if (historyIndex > 0) {
|
|
63
|
+
const newIndex = historyIndex - 1;
|
|
64
|
+
setHistoryIndex(newIndex);
|
|
65
|
+
lastSavedDataRef.current = JSON.stringify(history[newIndex]);
|
|
66
|
+
onDataChange(history[newIndex]);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const handleRedo = () => {
|
|
70
|
+
if (historyIndex < history.length - 1) {
|
|
71
|
+
const newIndex = historyIndex + 1;
|
|
72
|
+
setHistoryIndex(newIndex);
|
|
73
|
+
lastSavedDataRef.current = JSON.stringify(history[newIndex]);
|
|
74
|
+
onDataChange(history[newIndex]);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// Keyboard shortcuts for undo/redo
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const handleKeyDown = (e) => {
|
|
80
|
+
// Undo: Ctrl+Z (Cmd+Z on Mac)
|
|
81
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
handleUndo();
|
|
84
|
+
}
|
|
85
|
+
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
|
86
|
+
else if ((e.ctrlKey || e.metaKey) && (e.shiftKey && e.key === 'z' || e.key === 'y')) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
handleRedo();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
92
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
93
|
+
}, [historyIndex, history]);
|
|
94
|
+
// Throttled history update when currentData changes
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const currentDataStr = JSON.stringify(currentData);
|
|
97
|
+
// Skip if data hasn't actually changed
|
|
98
|
+
if (currentDataStr === lastSavedDataRef.current) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Clear existing throttle timeout
|
|
102
|
+
if (throttleTimeoutRef.current) {
|
|
103
|
+
clearTimeout(throttleTimeoutRef.current);
|
|
104
|
+
}
|
|
105
|
+
// Set new throttled update
|
|
106
|
+
throttleTimeoutRef.current = setTimeout(() => {
|
|
107
|
+
lastSavedDataRef.current = currentDataStr;
|
|
108
|
+
setHistory(prev => {
|
|
109
|
+
// Slice history at current index (discard future states)
|
|
110
|
+
const newHistory = prev.slice(0, historyIndex + 1);
|
|
111
|
+
// Add new state
|
|
112
|
+
newHistory.push(currentData);
|
|
113
|
+
// Limit history size to 50 states
|
|
114
|
+
if (newHistory.length > 50) {
|
|
115
|
+
newHistory.shift();
|
|
116
|
+
return newHistory;
|
|
117
|
+
}
|
|
118
|
+
return newHistory;
|
|
119
|
+
});
|
|
120
|
+
setHistoryIndex(prev => {
|
|
121
|
+
const newHistory = history.slice(0, prev + 1);
|
|
122
|
+
newHistory.push(currentData);
|
|
123
|
+
return Math.min(newHistory.length - 1, 49);
|
|
124
|
+
});
|
|
125
|
+
}, 500); // 500ms throttle
|
|
126
|
+
return () => {
|
|
127
|
+
if (throttleTimeoutRef.current) {
|
|
128
|
+
clearTimeout(throttleTimeoutRef.current);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}, [currentData, historyIndex, history]);
|
|
132
|
+
const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
const prefab = yield loadJson();
|
|
134
|
+
if (prefab) {
|
|
135
|
+
onDataChange(prefab);
|
|
136
|
+
// Reset history when loading new file
|
|
137
|
+
setHistory([prefab]);
|
|
138
|
+
setHistoryIndex(0);
|
|
139
|
+
lastSavedDataRef.current = JSON.stringify(prefab);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const canUndo = historyIndex > 0;
|
|
143
|
+
const canRedo = historyIndex < history.length - 1;
|
|
144
|
+
return _jsxs("div", { style: {
|
|
145
|
+
position: "absolute",
|
|
146
|
+
top: 8,
|
|
147
|
+
left: "50%",
|
|
148
|
+
transform: "translateX(-50%)",
|
|
149
|
+
display: "flex",
|
|
150
|
+
alignItems: "center",
|
|
151
|
+
gap: 6,
|
|
152
|
+
padding: "2px 4px",
|
|
153
|
+
background: "rgba(0,0,0,0.55)",
|
|
154
|
+
border: "1px solid rgba(255,255,255,0.12)",
|
|
155
|
+
borderRadius: 4,
|
|
156
|
+
color: "rgba(255,255,255,0.9)",
|
|
157
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
158
|
+
fontSize: 11,
|
|
159
|
+
lineHeight: 1,
|
|
160
|
+
WebkitUserSelect: "none",
|
|
161
|
+
userSelect: "none",
|
|
162
|
+
}, children: [_jsx(PanelButton, { onClick: () => onEditModeChange(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleUndo, disabled: !canUndo, title: "Undo (Ctrl+Z)", children: "\u21B6" }), _jsx(PanelButton, { onClick: handleRedo, disabled: !canRedo, title: "Redo (Ctrl+Shift+Z)", children: "\u21B7" }), _jsx("span", { style: { opacity: 0.35 }, children: "|" }), _jsx(PanelButton, { onClick: handleLoad, title: "Load JSON", children: "\uD83D\uDCE5" }), _jsx(PanelButton, { onClick: () => saveJson(currentData, "prefab"), title: "Save JSON", children: "\uD83D\uDCBE" })] });
|
|
163
|
+
};
|
|
164
|
+
const PanelButton = ({ onClick, disabled, title, children }) => {
|
|
165
|
+
return _jsx("button", { style: {
|
|
166
|
+
padding: "2px 6px",
|
|
167
|
+
font: "inherit",
|
|
168
|
+
background: "transparent",
|
|
169
|
+
color: disabled ? "rgba(255,255,255,0.3)" : "inherit",
|
|
170
|
+
border: "1px solid rgba(255,255,255,0.18)",
|
|
171
|
+
borderRadius: 3,
|
|
172
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
173
|
+
opacity: disabled ? 0.5 : 1,
|
|
174
|
+
}, onClick: onClick, disabled: disabled, title: title, onPointerEnter: (e) => {
|
|
175
|
+
if (!disabled) {
|
|
176
|
+
e.currentTarget.style.background = "rgba(255,255,255,0.08)";
|
|
177
|
+
}
|
|
178
|
+
}, onPointerLeave: (e) => {
|
|
179
|
+
e.currentTarget.style.background = "transparent";
|
|
180
|
+
}, children: children });
|
|
58
181
|
};
|
|
59
182
|
const saveJson = (data, filename) => {
|
|
60
183
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
@@ -188,7 +188,7 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
188
188
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
189
189
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
190
190
|
const isModelAvailable = !!(modelComp && modelComp.properties && modelComp.properties.filename && ctx.loadedModels[modelComp.properties.filename]);
|
|
191
|
-
// Generic component views (exclude geometry/material/model)
|
|
191
|
+
// Generic component views (exclude geometry/material/model/transform/physics)
|
|
192
192
|
const contextProps = {
|
|
193
193
|
loadedModels: ctx.loadedModels,
|
|
194
194
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -197,29 +197,48 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
|
|
|
197
197
|
parentMatrix,
|
|
198
198
|
registerRef: ctx.registerRef,
|
|
199
199
|
};
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
// Separate wrapper components (that accept children) from leaf components
|
|
201
|
+
const wrapperComponents = [];
|
|
202
|
+
const leafComponents = [];
|
|
203
|
+
if (gameObject.components) {
|
|
204
|
+
Object.entries(gameObject.components)
|
|
202
205
|
.filter(([key]) => key !== 'geometry' && key !== 'material' && key !== 'model' && key !== 'transform' && key !== 'physics')
|
|
203
|
-
.
|
|
206
|
+
.forEach(([key, comp]) => {
|
|
204
207
|
if (!comp || !comp.type)
|
|
205
|
-
return
|
|
208
|
+
return;
|
|
206
209
|
const def = getComponent(comp.type);
|
|
207
210
|
if (!def || !def.View)
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
return;
|
|
212
|
+
// Check if the component View accepts children by checking function signature
|
|
213
|
+
// Components that wrap content should accept children prop
|
|
214
|
+
const viewString = def.View.toString();
|
|
215
|
+
if (viewString.includes('children')) {
|
|
216
|
+
wrapperComponents.push({ key, View: def.View, properties: comp.properties });
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
leafComponents.push(_jsx(def.View, Object.assign({ properties: comp.properties }, contextProps), key));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Build the core content (model or mesh)
|
|
224
|
+
let coreContent;
|
|
212
225
|
// If we have a model (non-instanced) render it as a primitive with material override
|
|
213
226
|
if (isModelAvailable) {
|
|
214
227
|
const modelObj = ctx.loadedModels[modelComp.properties.filename].clone();
|
|
215
|
-
|
|
228
|
+
coreContent = (_jsxs("primitive", { object: modelObj, children: [material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), leafComponents] }));
|
|
229
|
+
}
|
|
230
|
+
else if (geometry && geometryDef && geometryDef.View) {
|
|
231
|
+
// Otherwise, if geometry present, render a mesh
|
|
232
|
+
coreContent = (_jsxs("mesh", { children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps), "geometry"), material && materialDef && materialDef.View && (_jsx(materialDef.View, { properties: material.properties, loadedTextures: ctx.loadedTextures, isSelected: ctx.selectedId === gameObject.id, editMode: ctx.editMode, parentMatrix: parentMatrix, registerRef: ctx.registerRef }, "material")), leafComponents] }));
|
|
216
233
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
else {
|
|
235
|
+
// No geometry or model, just render leaf components
|
|
236
|
+
coreContent = _jsx(_Fragment, { children: leafComponents });
|
|
220
237
|
}
|
|
221
|
-
//
|
|
222
|
-
return
|
|
238
|
+
// Wrap core content with wrapper components (in order)
|
|
239
|
+
return wrapperComponents.reduce((content, { key, View, properties }) => {
|
|
240
|
+
return _jsx(View, Object.assign({ properties: properties }, contextProps, { children: content }), key);
|
|
241
|
+
}, coreContent);
|
|
223
242
|
}
|
|
224
243
|
// Helper: wrap core content with physics component when necessary
|
|
225
244
|
function wrapPhysicsIfNeeded(gameObject, content, ctx) {
|
|
@@ -5,6 +5,8 @@ export interface Component {
|
|
|
5
5
|
component: any;
|
|
6
6
|
onUpdate: (newComp: any) => void;
|
|
7
7
|
basePath?: string;
|
|
8
|
+
transformMode?: "translate" | "rotate" | "scale";
|
|
9
|
+
setTransformMode?: (m: "translate" | "rotate" | "scale") => void;
|
|
8
10
|
}>;
|
|
9
11
|
defaultProperties: any;
|
|
10
12
|
View?: FC<any>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
function RotatorComponentEditor({ component, onUpdate }) {
|
|
5
|
+
var _a, _b;
|
|
6
|
+
const props = {
|
|
7
|
+
speed: (_a = component.properties.speed) !== null && _a !== void 0 ? _a : 1.0,
|
|
8
|
+
axis: (_b = component.properties.axis) !== null && _b !== void 0 ? _b : 'y'
|
|
9
|
+
};
|
|
10
|
+
return _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Rotation Speed" }), _jsx("input", { type: "number", step: "0.1", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.speed, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { speed: parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Rotation Axis" }), _jsxs("select", { className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: props.axis, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { axis: e.target.value })), children: [_jsx("option", { value: "x", children: "X" }), _jsx("option", { value: "y", children: "Y" }), _jsx("option", { value: "z", children: "Z" })] })] })] });
|
|
11
|
+
}
|
|
12
|
+
// The view component for Rotator
|
|
13
|
+
function RotatorView({ properties, children }) {
|
|
14
|
+
var _a, _b;
|
|
15
|
+
const groupRef = useRef(null);
|
|
16
|
+
const speed = (_a = properties.speed) !== null && _a !== void 0 ? _a : 1.0;
|
|
17
|
+
const axis = (_b = properties.axis) !== null && _b !== void 0 ? _b : 'y';
|
|
18
|
+
useFrame((state, delta) => {
|
|
19
|
+
if (groupRef.current) {
|
|
20
|
+
if (axis === 'x') {
|
|
21
|
+
groupRef.current.rotation.x += delta * speed;
|
|
22
|
+
}
|
|
23
|
+
else if (axis === 'y') {
|
|
24
|
+
groupRef.current.rotation.y += delta * speed;
|
|
25
|
+
}
|
|
26
|
+
else if (axis === 'z') {
|
|
27
|
+
groupRef.current.rotation.z += delta * speed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return (_jsx("group", { ref: groupRef, children: children }));
|
|
32
|
+
}
|
|
33
|
+
const RotatorComponent = {
|
|
34
|
+
name: 'Rotator',
|
|
35
|
+
Editor: RotatorComponentEditor,
|
|
36
|
+
View: RotatorView,
|
|
37
|
+
defaultProperties: {
|
|
38
|
+
speed: 1.0,
|
|
39
|
+
axis: 'y'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export default RotatorComponent;
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
function TransformComponentEditor({ component, onUpdate }) {
|
|
3
|
-
|
|
2
|
+
function TransformComponentEditor({ component, onUpdate, transformMode, setTransformMode }) {
|
|
3
|
+
const s = {
|
|
4
|
+
button: {
|
|
5
|
+
padding: '2px 6px',
|
|
6
|
+
background: 'transparent',
|
|
7
|
+
color: 'rgba(255,255,255,0.9)',
|
|
8
|
+
border: '1px solid rgba(255,255,255,0.14)',
|
|
9
|
+
borderRadius: 4,
|
|
10
|
+
cursor: 'pointer',
|
|
11
|
+
font: 'inherit',
|
|
12
|
+
},
|
|
13
|
+
buttonActive: {
|
|
14
|
+
background: 'rgba(255,255,255,0.10)',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
return _jsxs("div", { className: "flex flex-col", children: [transformMode && setTransformMode && (_jsxs("div", { className: "mb-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", 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
|
+
if (transformMode !== mode)
|
|
19
|
+
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
20
|
+
}, onPointerLeave: (e) => {
|
|
21
|
+
if (transformMode !== mode)
|
|
22
|
+
e.currentTarget.style.background = 'transparent';
|
|
23
|
+
}, children: mode }, mode))) })] })), _jsx(Vector3Input, { label: "Position", value: component.properties.position, onChange: v => onUpdate({ position: v }) }), _jsx(Vector3Input, { label: "Rotation", value: component.properties.rotation, onChange: v => onUpdate({ rotation: v }) }), _jsx(Vector3Input, { label: "Scale", value: component.properties.scale, onChange: v => onUpdate({ scale: v }) })] });
|
|
4
24
|
}
|
|
5
25
|
const TransformComponent = {
|
|
6
26
|
name: 'Transform',
|
|
@@ -18,5 +38,10 @@ export function Vector3Input({ label, value, onChange }) {
|
|
|
18
38
|
newValue[index] = parseFloat(val) || 0;
|
|
19
39
|
onChange(newValue);
|
|
20
40
|
};
|
|
21
|
-
|
|
41
|
+
const axes = [
|
|
42
|
+
{ key: 'x', color: 'red', index: 0 },
|
|
43
|
+
{ key: 'y', color: 'green', index: 1 },
|
|
44
|
+
{ key: 'z', color: 'blue', index: 2 }
|
|
45
|
+
];
|
|
46
|
+
return _jsxs("div", { className: "mb-2", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-1", children: label }), _jsx("div", { className: "flex gap-1", children: axes.map(({ key, color, index }) => (_jsxs("div", { className: "flex-1 flex items-center gap-1 bg-black/30 border border-cyan-500/20 rounded px-1.5 py-1 min-h-[32px]", children: [_jsx("span", { className: `text-xs font-bold text-${color}-400 w-3`, children: key.toUpperCase() }), _jsx("input", { className: "flex-1 bg-transparent text-xs text-cyan-200 font-mono outline-none w-full min-w-0", type: "number", step: "0.1", value: value[index].toFixed(2), onChange: e => handleChange(index, e.target.value), onFocus: e => e.target.select() })] }, key))) })] });
|
|
22
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-three-game",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Batteries included React Three Fiber game engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"type": "module",
|
|
18
18
|
"workspaces": ["docs"],
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@react-three/fiber": "
|
|
21
|
-
"@react-three/drei": "
|
|
22
|
-
"@react-three/rapier": "
|
|
23
|
-
"react": "
|
|
24
|
-
"react-dom": "
|
|
25
|
-
"three": "
|
|
20
|
+
"@react-three/fiber": ">=9.0.0",
|
|
21
|
+
"@react-three/drei": ">=10.0.0",
|
|
22
|
+
"@react-three/rapier": ">=2.0.0",
|
|
23
|
+
"react": ">=18.0.0",
|
|
24
|
+
"react-dom": ">=18.0.0",
|
|
25
|
+
"three": ">=0.181.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@react-three/drei": "^10.7.7",
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,10 @@ export {
|
|
|
10
10
|
SharedCanvas,
|
|
11
11
|
} from './tools/assetviewer/page';
|
|
12
12
|
|
|
13
|
+
// Component Registry
|
|
14
|
+
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
15
|
+
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
16
|
+
|
|
13
17
|
// Helpers
|
|
14
18
|
export * from './helpers';
|
|
15
19
|
|