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.
Files changed (64) hide show
  1. package/README.md +16 -3
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/shared/GameCanvas.js +1 -3
  5. package/dist/tools/assetviewer/page.js +35 -14
  6. package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
  7. package/dist/tools/prefabeditor/Dropdown.js +82 -0
  8. package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
  9. package/dist/tools/prefabeditor/EditorTree.js +149 -91
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  12. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
  14. package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
  16. package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
  17. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
  18. package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
  22. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
  23. package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
  24. package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
  25. package/dist/tools/prefabeditor/components/Input.js +73 -21
  26. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
  27. package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
  28. package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
  29. package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
  30. package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
  31. package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
  32. package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
  33. package/dist/tools/prefabeditor/components/index.js +5 -1
  34. package/dist/tools/prefabeditor/styles.d.ts +5 -2
  35. package/dist/tools/prefabeditor/styles.js +7 -3
  36. package/dist/tools/prefabeditor/utils.d.ts +4 -3
  37. package/dist/tools/prefabeditor/utils.js +53 -5
  38. package/package.json +1 -1
  39. package/react-three-game-skill/react-three-game/SKILL.md +4 -1
  40. package/src/index.ts +7 -0
  41. package/src/shared/GameCanvas.tsx +0 -3
  42. package/src/tools/assetviewer/page.tsx +77 -45
  43. package/src/tools/prefabeditor/Dropdown.tsx +112 -0
  44. package/src/tools/prefabeditor/EditorContext.tsx +5 -0
  45. package/src/tools/prefabeditor/EditorTree.tsx +242 -178
  46. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  47. package/src/tools/prefabeditor/EditorUI.tsx +1 -1
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
  55. package/src/tools/prefabeditor/components/Input.tsx +220 -27
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
  62. package/src/tools/prefabeditor/components/index.ts +5 -1
  63. package/src/tools/prefabeditor/styles.ts +7 -3
  64. 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={{ ...inspector.content, paddingRight: 2 }} className="prefab-scroll">
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
- {editMode && <EditorUI
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