react-three-game 0.0.56 → 0.0.58
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 +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- package/src/tools/prefabeditor/utils.ts +55 -4
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Prefab } from './types';
|
|
4
|
+
import { menu } from './styles';
|
|
5
|
+
import { useEditorContext } from './EditorContext';
|
|
6
|
+
import { getComponent } from './components/ComponentRegistry';
|
|
7
|
+
import { loadJson, saveJson, regenerateIds, updateNodeById } from './utils';
|
|
8
|
+
|
|
9
|
+
export type TreeContextMenuState = { nodeId: string; x: number; y: number } | null;
|
|
10
|
+
|
|
11
|
+
function createEmptyPrefab(): Prefab {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
name: 'New Scene',
|
|
15
|
+
root: {
|
|
16
|
+
id: crypto.randomUUID(),
|
|
17
|
+
name: 'Scene',
|
|
18
|
+
components: {
|
|
19
|
+
transform: {
|
|
20
|
+
type: 'Transform',
|
|
21
|
+
properties: { ...getComponent('Transform')?.defaultProperties }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
children: []
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function MenuPanel({
|
|
30
|
+
children,
|
|
31
|
+
style,
|
|
32
|
+
}: {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
style?: React.CSSProperties;
|
|
35
|
+
}) {
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ ...menu.container, position: 'static', ...style }} onClick={(e) => e.stopPropagation()}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function MenuItemButton({
|
|
44
|
+
children,
|
|
45
|
+
onClick,
|
|
46
|
+
danger = false,
|
|
47
|
+
style,
|
|
48
|
+
}: {
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
onClick: () => void;
|
|
51
|
+
danger?: boolean;
|
|
52
|
+
style?: React.CSSProperties;
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
style={danger ? { ...menu.item, ...menu.danger, ...style } : { ...menu.item, ...style }}
|
|
57
|
+
onClick={onClick}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</button>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function MenuSubmenu({
|
|
65
|
+
label,
|
|
66
|
+
children,
|
|
67
|
+
}: {
|
|
68
|
+
label: string;
|
|
69
|
+
children: React.ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
style={{ position: 'relative' }}
|
|
76
|
+
onMouseEnter={() => setIsOpen(true)}
|
|
77
|
+
onMouseLeave={() => setIsOpen(false)}
|
|
78
|
+
>
|
|
79
|
+
<MenuItemButton
|
|
80
|
+
onClick={() => setIsOpen(open => !open)}
|
|
81
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}
|
|
82
|
+
>
|
|
83
|
+
<span>{label}</span>
|
|
84
|
+
<span aria-hidden="true">›</span>
|
|
85
|
+
</MenuItemButton>
|
|
86
|
+
{isOpen && (
|
|
87
|
+
<div
|
|
88
|
+
style={{
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
top: 0,
|
|
91
|
+
left: '100%',
|
|
92
|
+
zIndex: 1,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<MenuPanel>{children}</MenuPanel>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function MenuTriggerButton({
|
|
103
|
+
buttonRef,
|
|
104
|
+
onToggle,
|
|
105
|
+
title,
|
|
106
|
+
style,
|
|
107
|
+
children,
|
|
108
|
+
}: {
|
|
109
|
+
buttonRef: React.RefObject<HTMLButtonElement | null>;
|
|
110
|
+
onToggle: () => void;
|
|
111
|
+
title: string;
|
|
112
|
+
style: React.CSSProperties;
|
|
113
|
+
children: React.ReactNode;
|
|
114
|
+
}) {
|
|
115
|
+
return (
|
|
116
|
+
<button
|
|
117
|
+
ref={buttonRef}
|
|
118
|
+
style={style}
|
|
119
|
+
onClick={(e) => {
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
onToggle();
|
|
122
|
+
}}
|
|
123
|
+
title={title}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function TreeNodeMenu({
|
|
131
|
+
isRoot,
|
|
132
|
+
nodeId,
|
|
133
|
+
onAddChild,
|
|
134
|
+
onFocus,
|
|
135
|
+
onDuplicate,
|
|
136
|
+
onDelete,
|
|
137
|
+
onClose,
|
|
138
|
+
}: {
|
|
139
|
+
isRoot: boolean;
|
|
140
|
+
nodeId: string;
|
|
141
|
+
onAddChild: (parentId: string) => void;
|
|
142
|
+
onFocus: (nodeId: string) => void;
|
|
143
|
+
onDuplicate?: (nodeId: string) => void;
|
|
144
|
+
onDelete?: (nodeId: string) => void;
|
|
145
|
+
onClose: () => void;
|
|
146
|
+
}) {
|
|
147
|
+
return (
|
|
148
|
+
<MenuPanel>
|
|
149
|
+
<MenuItemButton onClick={() => { onAddChild(nodeId); onClose(); }}>
|
|
150
|
+
Add Child
|
|
151
|
+
</MenuItemButton>
|
|
152
|
+
<MenuItemButton onClick={() => { onFocus(nodeId); onClose(); }}>
|
|
153
|
+
Focus Camera
|
|
154
|
+
</MenuItemButton>
|
|
155
|
+
{!isRoot && onDuplicate && (
|
|
156
|
+
<MenuItemButton onClick={() => { onDuplicate(nodeId); onClose(); }}>
|
|
157
|
+
Duplicate
|
|
158
|
+
</MenuItemButton>
|
|
159
|
+
)}
|
|
160
|
+
{!isRoot && onDelete && (
|
|
161
|
+
<MenuItemButton danger onClick={() => { onDelete(nodeId); onClose(); }}>
|
|
162
|
+
Delete
|
|
163
|
+
</MenuItemButton>
|
|
164
|
+
)}
|
|
165
|
+
</MenuPanel>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function TreeContextMenu({
|
|
170
|
+
contextMenu,
|
|
171
|
+
onClose,
|
|
172
|
+
children,
|
|
173
|
+
}: {
|
|
174
|
+
contextMenu: TreeContextMenuState;
|
|
175
|
+
onClose: () => void;
|
|
176
|
+
children: (nodeId: string, onClose: () => void) => React.ReactNode;
|
|
177
|
+
}) {
|
|
178
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
179
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!contextMenu) return;
|
|
183
|
+
|
|
184
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
185
|
+
const target = event.target as Node | null;
|
|
186
|
+
if (!target) return;
|
|
187
|
+
if (panelRef.current?.contains(target)) return;
|
|
188
|
+
onClose();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
192
|
+
if (event.key === 'Escape') onClose();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
196
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
197
|
+
|
|
198
|
+
return () => {
|
|
199
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
200
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
201
|
+
};
|
|
202
|
+
}, [contextMenu, onClose]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!contextMenu || !panelRef.current || typeof window === 'undefined') return;
|
|
206
|
+
|
|
207
|
+
const panelRect = panelRef.current.getBoundingClientRect();
|
|
208
|
+
const left = Math.max(8, Math.min(contextMenu.x, window.innerWidth - panelRect.width - 8));
|
|
209
|
+
const top = Math.max(8, Math.min(contextMenu.y, window.innerHeight - panelRect.height - 8));
|
|
210
|
+
setPosition({ left, top });
|
|
211
|
+
}, [contextMenu]);
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!contextMenu) {
|
|
215
|
+
setPosition(null);
|
|
216
|
+
}
|
|
217
|
+
}, [contextMenu]);
|
|
218
|
+
|
|
219
|
+
if (!contextMenu || typeof document === 'undefined') return null;
|
|
220
|
+
|
|
221
|
+
return createPortal(
|
|
222
|
+
<div
|
|
223
|
+
ref={panelRef}
|
|
224
|
+
style={{
|
|
225
|
+
position: 'fixed',
|
|
226
|
+
left: position?.left ?? contextMenu.x,
|
|
227
|
+
top: position?.top ?? contextMenu.y,
|
|
228
|
+
zIndex: 1000,
|
|
229
|
+
}}
|
|
230
|
+
onMouseLeave={onClose}
|
|
231
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
232
|
+
>
|
|
233
|
+
{children(contextMenu.nodeId, onClose)}
|
|
234
|
+
</div>,
|
|
235
|
+
document.body
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function FileMenu({
|
|
240
|
+
prefabData,
|
|
241
|
+
setPrefabData,
|
|
242
|
+
onClose
|
|
243
|
+
}: {
|
|
244
|
+
prefabData: Prefab;
|
|
245
|
+
setPrefabData: Dispatch<SetStateAction<Prefab>>;
|
|
246
|
+
onClose: () => void;
|
|
247
|
+
}) {
|
|
248
|
+
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
249
|
+
|
|
250
|
+
const handleNewScene = () => {
|
|
251
|
+
setPrefabData(createEmptyPrefab());
|
|
252
|
+
onClose();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const handleNewSceneFromPrefab = async () => {
|
|
256
|
+
const loadedPrefab = await loadJson();
|
|
257
|
+
if (!loadedPrefab) return;
|
|
258
|
+
setPrefabData(loadedPrefab);
|
|
259
|
+
onClose();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const handleSave = () => {
|
|
263
|
+
saveJson(prefabData, 'prefab');
|
|
264
|
+
onClose();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const handleLoadIntoScene = async () => {
|
|
268
|
+
const loadedPrefab = await loadJson();
|
|
269
|
+
if (!loadedPrefab) return;
|
|
270
|
+
|
|
271
|
+
setPrefabData(prev => ({
|
|
272
|
+
...prev,
|
|
273
|
+
root: updateNodeById(prev.root, prev.root.id, root => ({
|
|
274
|
+
...root,
|
|
275
|
+
children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
|
|
276
|
+
}))
|
|
277
|
+
}));
|
|
278
|
+
onClose();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<MenuPanel style={{ overflow: 'visible' }}>
|
|
283
|
+
<MenuSubmenu label="File">
|
|
284
|
+
<MenuItemButton onClick={handleNewScene}>
|
|
285
|
+
New Scene
|
|
286
|
+
</MenuItemButton>
|
|
287
|
+
<MenuItemButton onClick={handleNewSceneFromPrefab}>
|
|
288
|
+
New Scene from Prefab
|
|
289
|
+
</MenuItemButton>
|
|
290
|
+
<MenuItemButton onClick={handleLoadIntoScene}>
|
|
291
|
+
Load Prefab into Scene
|
|
292
|
+
</MenuItemButton>
|
|
293
|
+
<MenuItemButton onClick={handleSave}>
|
|
294
|
+
Save Prefab
|
|
295
|
+
</MenuItemButton>
|
|
296
|
+
</MenuSubmenu>
|
|
297
|
+
<MenuSubmenu label="Export">
|
|
298
|
+
<MenuItemButton onClick={() => { onExportGLB?.(); onClose(); }}>
|
|
299
|
+
GLB
|
|
300
|
+
</MenuItemButton>
|
|
301
|
+
<MenuItemButton onClick={() => { onScreenshot?.(); onClose(); }}>
|
|
302
|
+
PNG
|
|
303
|
+
</MenuItemButton>
|
|
304
|
+
</MenuSubmenu>
|
|
305
|
+
</MenuPanel>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -97,7 +97,7 @@ function NodeInspector({
|
|
|
97
97
|
if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
|
|
98
98
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
99
99
|
|
|
100
|
-
return <div style={
|
|
100
|
+
return <div style={inspector.content} className="prefab-scroll">
|
|
101
101
|
{/* Node Name */}
|
|
102
102
|
<div style={base.section}>
|
|
103
103
|
<div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
|
|
@@ -36,13 +36,16 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
36
36
|
initialPrefab?: Prefab;
|
|
37
37
|
physics?: boolean;
|
|
38
38
|
onPrefabChange?: (prefab: Prefab) => void;
|
|
39
|
+
uiPlugins?: React.ReactNode[] | React.ReactNode;
|
|
39
40
|
children?: React.ReactNode;
|
|
40
|
-
}>(({ basePath, initialPrefab, physics = true, onPrefabChange, children }, ref) => {
|
|
41
|
+
}>(({ basePath, initialPrefab, physics = true, onPrefabChange, uiPlugins, children }, ref) => {
|
|
41
42
|
const [editMode, setEditMode] = useState(true);
|
|
42
43
|
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? DEFAULT_PREFAB);
|
|
43
44
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
44
45
|
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
|
|
45
46
|
const [snapResolution, setSnapResolution] = useState(0);
|
|
47
|
+
const [positionSnap, setPositionSnap] = useState(0.5);
|
|
48
|
+
const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
|
|
46
49
|
const [history, setHistory] = useState<Prefab[]>([loadedPrefab]);
|
|
47
50
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
48
51
|
const throttleRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -121,6 +124,10 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
121
124
|
});
|
|
122
125
|
};
|
|
123
126
|
|
|
127
|
+
const handleFocusNode = (nodeId: string) => {
|
|
128
|
+
prefabRootRef.current?.focusNode(nodeId);
|
|
129
|
+
};
|
|
130
|
+
|
|
124
131
|
useEffect(() => {
|
|
125
132
|
const canvas = document.querySelector('canvas');
|
|
126
133
|
if (canvas) canvasRef.current = canvas;
|
|
@@ -214,10 +221,15 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
214
221
|
setTransformMode,
|
|
215
222
|
snapResolution,
|
|
216
223
|
setSnapResolution,
|
|
224
|
+
positionSnap,
|
|
225
|
+
setPositionSnap,
|
|
226
|
+
rotationSnap,
|
|
227
|
+
setRotationSnap,
|
|
228
|
+
onFocusNode: handleFocusNode,
|
|
217
229
|
onScreenshot: handleScreenshot,
|
|
218
230
|
onExportGLB: handleExportGLB
|
|
219
231
|
}}>
|
|
220
|
-
<GameCanvas>
|
|
232
|
+
<GameCanvas camera={{ position: [0, 5, 15] }}>
|
|
221
233
|
{physics ? (
|
|
222
234
|
<Physics debug={editMode} paused={editMode}>
|
|
223
235
|
{content}
|
|
@@ -229,8 +241,9 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
229
241
|
<button style={base.btn} onClick={() => setEditMode(!editMode)}>
|
|
230
242
|
{editMode ? "▶" : "⏸"}
|
|
231
243
|
</button>
|
|
244
|
+
{uiPlugins}
|
|
232
245
|
</div>
|
|
233
|
-
|
|
246
|
+
<EditorUI
|
|
234
247
|
prefabData={loadedPrefab}
|
|
235
248
|
setPrefabData={updatePrefab}
|
|
236
249
|
selectedId={selectedId}
|
|
@@ -240,7 +253,7 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
240
253
|
onRedo={redo}
|
|
241
254
|
canUndo={historyIndex > 0}
|
|
242
255
|
canRedo={historyIndex < history.length - 1}
|
|
243
|
-
/>
|
|
256
|
+
/>
|
|
244
257
|
</EditorContext.Provider>
|
|
245
258
|
});
|
|
246
259
|
|