react-three-game 0.0.55 → 0.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/ContactShadow.d.ts +8 -0
- package/dist/shared/ContactShadow.js +32 -0
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +36 -15
- package/dist/tools/dragdrop/DragDropLoader.js +17 -40
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -0
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- 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 +139 -70
- package/dist/tools/prefabeditor/EditorUI.js +5 -10
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +70 -3
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +136 -35
- 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 +25 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
- 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 +100 -47
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -14
- 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 +6 -11
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +31 -19
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +17 -4
- package/dist/tools/prefabeditor/styles.js +69 -32
- package/dist/tools/prefabeditor/utils.d.ts +8 -3
- package/dist/tools/prefabeditor/utils.js +92 -6
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +6 -0
- package/src/index.ts +7 -0
- package/src/shared/ContactShadow.tsx +74 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +78 -46
- package/src/tools/dragdrop/DragDropLoader.tsx +7 -39
- package/src/tools/dragdrop/modelLoader.ts +36 -0
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +237 -115
- package/src/tools/prefabeditor/EditorUI.tsx +6 -11
- package/src/tools/prefabeditor/PrefabEditor.tsx +77 -5
- package/src/tools/prefabeditor/PrefabRoot.tsx +228 -59
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +80 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +2 -2
- 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 +247 -53
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +191 -20
- package/src/tools/prefabeditor/components/ModelComponent.tsx +52 -5
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +14 -16
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +78 -20
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +71 -32
- package/src/tools/prefabeditor/utils.ts +96 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
type Placement = 'bottom-start' | 'bottom-end' | 'left-start' | 'right-start';
|
|
5
|
+
|
|
6
|
+
export function Dropdown({
|
|
7
|
+
trigger,
|
|
8
|
+
children,
|
|
9
|
+
placement = 'bottom-end',
|
|
10
|
+
offset = 6,
|
|
11
|
+
zIndex = 1000,
|
|
12
|
+
}: {
|
|
13
|
+
trigger: (props: { ref: React.RefObject<HTMLButtonElement | null>; isOpen: boolean; toggle: () => void; close: () => void; }) => ReactNode;
|
|
14
|
+
children: ReactNode | ((close: () => void) => ReactNode);
|
|
15
|
+
placement?: Placement;
|
|
16
|
+
offset?: number;
|
|
17
|
+
zIndex?: number;
|
|
18
|
+
}) {
|
|
19
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
|
|
21
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
22
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
const close = () => setIsOpen(false);
|
|
25
|
+
const toggle = () => setIsOpen(prev => !prev);
|
|
26
|
+
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
if (!isOpen || !triggerRef.current || !panelRef.current || typeof window === 'undefined') return;
|
|
29
|
+
|
|
30
|
+
const updatePosition = () => {
|
|
31
|
+
const triggerRect = triggerRef.current?.getBoundingClientRect();
|
|
32
|
+
const panelRect = panelRef.current?.getBoundingClientRect();
|
|
33
|
+
if (!triggerRect || !panelRect) return;
|
|
34
|
+
|
|
35
|
+
let left = triggerRect.left;
|
|
36
|
+
let top = triggerRect.bottom + offset;
|
|
37
|
+
|
|
38
|
+
if (placement === 'bottom-end') {
|
|
39
|
+
left = triggerRect.right - panelRect.width;
|
|
40
|
+
top = triggerRect.bottom + offset;
|
|
41
|
+
} else if (placement === 'bottom-start') {
|
|
42
|
+
left = triggerRect.left;
|
|
43
|
+
top = triggerRect.bottom + offset;
|
|
44
|
+
} else if (placement === 'left-start') {
|
|
45
|
+
left = triggerRect.left - panelRect.width - offset;
|
|
46
|
+
top = triggerRect.top;
|
|
47
|
+
} else if (placement === 'right-start') {
|
|
48
|
+
left = triggerRect.right + offset;
|
|
49
|
+
top = triggerRect.top;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
left = Math.max(8, Math.min(left, window.innerWidth - panelRect.width - 8));
|
|
53
|
+
top = Math.max(8, Math.min(top, window.innerHeight - panelRect.height - 8));
|
|
54
|
+
|
|
55
|
+
setPosition({ left, top });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
updatePosition();
|
|
59
|
+
window.addEventListener('resize', updatePosition);
|
|
60
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
window.removeEventListener('resize', updatePosition);
|
|
64
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
65
|
+
};
|
|
66
|
+
}, [isOpen, placement, offset]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isOpen) return;
|
|
70
|
+
|
|
71
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
72
|
+
const target = event.target as Node | null;
|
|
73
|
+
if (!target) return;
|
|
74
|
+
if (triggerRef.current?.contains(target)) return;
|
|
75
|
+
if (panelRef.current?.contains(target)) return;
|
|
76
|
+
close();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
80
|
+
if (event.key === 'Escape') close();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
84
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
88
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
89
|
+
};
|
|
90
|
+
}, [isOpen]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{trigger({ ref: triggerRef, isOpen, toggle, close })}
|
|
95
|
+
{isOpen && typeof document !== 'undefined' && createPortal(
|
|
96
|
+
<div
|
|
97
|
+
ref={panelRef}
|
|
98
|
+
onMouseLeave={close}
|
|
99
|
+
style={{
|
|
100
|
+
position: 'fixed',
|
|
101
|
+
left: position?.left ?? -9999,
|
|
102
|
+
top: position?.top ?? -9999,
|
|
103
|
+
zIndex,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{typeof children === 'function' ? children(close) : children}
|
|
107
|
+
</div>,
|
|
108
|
+
document.body
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -5,6 +5,11 @@ interface EditorContextType {
|
|
|
5
5
|
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
6
6
|
snapResolution: number;
|
|
7
7
|
setSnapResolution: (resolution: number) => void;
|
|
8
|
+
positionSnap: number;
|
|
9
|
+
setPositionSnap: (resolution: number) => void;
|
|
10
|
+
rotationSnap: number;
|
|
11
|
+
setRotationSnap: (resolution: number) => void;
|
|
12
|
+
onFocusNode?: (nodeId: string) => void;
|
|
8
13
|
onScreenshot?: () => void;
|
|
9
14
|
onExportGLB?: () => void;
|
|
10
15
|
}
|
|
@@ -1,9 +1,79 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
|
|
2
2
|
import { Prefab, GameObject } from "./types";
|
|
3
3
|
import { getComponent } from './components/ComponentRegistry';
|
|
4
|
-
import { base, tree, menu } from './styles';
|
|
4
|
+
import { base, colors, tree, menu } from './styles';
|
|
5
5
|
import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
|
|
6
6
|
import { useEditorContext } from './EditorContext';
|
|
7
|
+
import { Dropdown } from './Dropdown';
|
|
8
|
+
|
|
9
|
+
type DropPosition = 'before' | 'inside';
|
|
10
|
+
|
|
11
|
+
function moveNode(root: GameObject, draggedId: string, targetId: string, position: DropPosition): GameObject {
|
|
12
|
+
const draggedNode = findNode(root, draggedId);
|
|
13
|
+
const oldParent = findParent(root, draggedId);
|
|
14
|
+
|
|
15
|
+
if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (position === 'before') {
|
|
20
|
+
const targetParent = findParent(root, targetId);
|
|
21
|
+
if (!targetParent?.children) return root;
|
|
22
|
+
|
|
23
|
+
if (targetParent.id === oldParent.id) {
|
|
24
|
+
const siblings = targetParent.children.filter(child => child.id !== draggedId);
|
|
25
|
+
const targetIndex = siblings.findIndex(child => child.id === targetId);
|
|
26
|
+
if (targetIndex === -1) return root;
|
|
27
|
+
|
|
28
|
+
siblings.splice(targetIndex, 0, draggedNode);
|
|
29
|
+
return updateNodeById(root, targetParent.id, parent => ({ ...parent, children: siblings }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
|
|
33
|
+
...parent,
|
|
34
|
+
children: (parent.children ?? []).filter(child => child.id !== draggedId)
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
|
|
38
|
+
const children = [...(parent.children ?? [])];
|
|
39
|
+
const targetIndex = children.findIndex(child => child.id === targetId);
|
|
40
|
+
if (targetIndex === -1) return parent;
|
|
41
|
+
|
|
42
|
+
children.splice(targetIndex, 0, draggedNode);
|
|
43
|
+
return { ...parent, children };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
|
|
48
|
+
...parent,
|
|
49
|
+
children: (parent.children ?? []).filter(child => child.id !== draggedId)
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
return updateNodeById(rootWithoutDragged, targetId, target => ({
|
|
53
|
+
...target,
|
|
54
|
+
children: [...(target.children ?? []), draggedNode]
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function duplicateNodeBelow(root: GameObject, nodeId: string): { root: GameObject; duplicatedId: string } | null {
|
|
59
|
+
const node = findNode(root, nodeId);
|
|
60
|
+
const parent = findParent(root, nodeId);
|
|
61
|
+
if (!node || !parent) return null;
|
|
62
|
+
|
|
63
|
+
const duplicate = cloneNode(node);
|
|
64
|
+
const nextRoot = updateNodeById(root, parent.id, currentParent => ({
|
|
65
|
+
...currentParent,
|
|
66
|
+
children: (() => {
|
|
67
|
+
const children = [...(currentParent.children ?? [])];
|
|
68
|
+
const index = children.findIndex(child => child.id === nodeId);
|
|
69
|
+
if (index === -1) return [...children, duplicate];
|
|
70
|
+
children.splice(index + 1, 0, duplicate);
|
|
71
|
+
return children;
|
|
72
|
+
})()
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
return { root: nextRoot, duplicatedId: duplicate.id };
|
|
76
|
+
}
|
|
7
77
|
|
|
8
78
|
export default function EditorTree({
|
|
9
79
|
prefabData,
|
|
@@ -24,21 +94,15 @@ export default function EditorTree({
|
|
|
24
94
|
canUndo?: boolean;
|
|
25
95
|
canRedo?: boolean;
|
|
26
96
|
}) {
|
|
27
|
-
const
|
|
97
|
+
const { onFocusNode } = useEditorContext();
|
|
28
98
|
const [draggedId, setDraggedId] = useState<string | null>(null);
|
|
99
|
+
const [dropTarget, setDropTarget] = useState<{ id: string; position: DropPosition } | null>(null);
|
|
29
100
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
30
101
|
const [collapsed, setCollapsed] = useState(false);
|
|
31
|
-
const [fileMenuOpen, setFileMenuOpen] = useState(false);
|
|
32
102
|
const [searchQuery, setSearchQuery] = useState('');
|
|
33
103
|
|
|
34
104
|
if (!prefabData || !setPrefabData) return null;
|
|
35
105
|
|
|
36
|
-
const handleContextMenu = (e: MouseEvent, nodeId: string) => {
|
|
37
|
-
e.preventDefault();
|
|
38
|
-
e.stopPropagation();
|
|
39
|
-
setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
|
|
40
|
-
};
|
|
41
|
-
|
|
42
106
|
const toggleCollapse = (e: MouseEvent, id: string) => {
|
|
43
107
|
e.stopPropagation();
|
|
44
108
|
setCollapsedIds(prev => {
|
|
@@ -49,47 +113,46 @@ export default function EditorTree({
|
|
|
49
113
|
};
|
|
50
114
|
|
|
51
115
|
const handleAddChild = (parentId: string) => {
|
|
116
|
+
const newNode = {
|
|
117
|
+
id: crypto.randomUUID(),
|
|
118
|
+
name: "New Node",
|
|
119
|
+
components: {
|
|
120
|
+
transform: {
|
|
121
|
+
type: "Transform",
|
|
122
|
+
properties: { ...getComponent('Transform')?.defaultProperties }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
52
127
|
setPrefabData(prev => ({
|
|
53
128
|
...prev,
|
|
54
129
|
root: updateNodeById(prev.root, parentId, parent => ({
|
|
55
130
|
...parent,
|
|
56
|
-
children: [...(parent.children ?? []),
|
|
57
|
-
id: crypto.randomUUID(),
|
|
58
|
-
name: "New Node",
|
|
59
|
-
components: {
|
|
60
|
-
transform: {
|
|
61
|
-
type: "Transform",
|
|
62
|
-
properties: { ...getComponent('Transform')?.defaultProperties }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}]
|
|
131
|
+
children: [...(parent.children ?? []), newNode]
|
|
66
132
|
}))
|
|
67
133
|
}));
|
|
68
|
-
|
|
134
|
+
setSelectedId(newNode.id);
|
|
69
135
|
};
|
|
70
136
|
|
|
71
137
|
const handleDuplicate = (nodeId: string) => {
|
|
72
138
|
if (nodeId === prefabData.root.id) return;
|
|
73
139
|
setPrefabData(prev => {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
140
|
+
const result = duplicateNodeBelow(prev.root, nodeId);
|
|
141
|
+
if (!result) return prev;
|
|
142
|
+
|
|
143
|
+
setSelectedId(result.duplicatedId);
|
|
144
|
+
|
|
77
145
|
return {
|
|
78
146
|
...prev,
|
|
79
|
-
root:
|
|
80
|
-
...p,
|
|
81
|
-
children: [...(p.children ?? []), cloneNode(node)]
|
|
82
|
-
}))
|
|
147
|
+
root: result.root
|
|
83
148
|
};
|
|
84
149
|
});
|
|
85
|
-
setContextMenu(null);
|
|
86
150
|
};
|
|
87
151
|
|
|
88
152
|
const handleDelete = (nodeId: string) => {
|
|
89
153
|
if (nodeId === prefabData.root.id) return;
|
|
90
154
|
setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
|
|
91
155
|
if (selectedId === nodeId) setSelectedId(null);
|
|
92
|
-
setContextMenu(null);
|
|
93
156
|
};
|
|
94
157
|
|
|
95
158
|
const handleToggleDisabled = (nodeId: string) => {
|
|
@@ -100,7 +163,6 @@ export default function EditorTree({
|
|
|
100
163
|
disabled: !node.disabled
|
|
101
164
|
}))
|
|
102
165
|
}));
|
|
103
|
-
setContextMenu(null);
|
|
104
166
|
};
|
|
105
167
|
|
|
106
168
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
@@ -109,34 +171,36 @@ export default function EditorTree({
|
|
|
109
171
|
setDraggedId(id);
|
|
110
172
|
};
|
|
111
173
|
|
|
112
|
-
const
|
|
174
|
+
const getDropPosition = (e: React.DragEvent<HTMLDivElement>, isRoot: boolean): DropPosition => {
|
|
175
|
+
if (isRoot) return 'inside';
|
|
176
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
177
|
+
return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
|
|
113
181
|
if (!draggedId || draggedId === targetId) return;
|
|
114
182
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
115
183
|
if (draggedNode && findNode(draggedNode, targetId)) return;
|
|
116
184
|
e.preventDefault();
|
|
185
|
+
setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>, targetId: string) => {
|
|
189
|
+
const relatedTarget = e.relatedTarget;
|
|
190
|
+
if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget)) return;
|
|
191
|
+
setDropTarget(current => current?.id === targetId ? null : current);
|
|
117
192
|
};
|
|
118
193
|
|
|
119
|
-
const handleDrop = (e: React.DragEvent
|
|
194
|
+
const handleDrop = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
|
|
120
195
|
if (!draggedId || draggedId === targetId) return;
|
|
121
196
|
e.preventDefault();
|
|
197
|
+
const dropPosition = getDropPosition(e, isRoot);
|
|
122
198
|
setPrefabData(prev => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) return prev;
|
|
126
|
-
|
|
127
|
-
let root = updateNodeById(prev.root, oldParent.id, p => ({
|
|
128
|
-
...p,
|
|
129
|
-
children: p.children!.filter(c => c.id !== draggedId)
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
root = updateNodeById(root, targetId, t => ({
|
|
133
|
-
...t,
|
|
134
|
-
children: [...(t.children ?? []), draggedNode]
|
|
135
|
-
}));
|
|
136
|
-
|
|
137
|
-
return { ...prev, root };
|
|
199
|
+
const root = moveNode(prev.root, draggedId, targetId, dropPosition);
|
|
200
|
+
return root === prev.root ? prev : { ...prev, root };
|
|
138
201
|
});
|
|
139
202
|
setDraggedId(null);
|
|
203
|
+
setDropTarget(null);
|
|
140
204
|
};
|
|
141
205
|
|
|
142
206
|
|
|
@@ -156,6 +220,9 @@ export default function EditorTree({
|
|
|
156
220
|
const isCollapsed = collapsedIds.has(node.id);
|
|
157
221
|
const hasChildren = node.children && node.children.length > 0;
|
|
158
222
|
const isRoot = node.id === prefabData.root.id;
|
|
223
|
+
const isDropTarget = dropTarget?.id === node.id;
|
|
224
|
+
const showDropBefore = isDropTarget && dropTarget?.position === 'before';
|
|
225
|
+
const showDropInside = isDropTarget && dropTarget?.position === 'inside';
|
|
159
226
|
|
|
160
227
|
return (
|
|
161
228
|
<div key={node.id}>
|
|
@@ -168,14 +235,16 @@ export default function EditorTree({
|
|
|
168
235
|
display: 'flex',
|
|
169
236
|
alignItems: 'center',
|
|
170
237
|
justifyContent: 'space-between',
|
|
238
|
+
borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined,
|
|
239
|
+
boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined,
|
|
171
240
|
}}
|
|
172
241
|
draggable={!isRoot}
|
|
173
242
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
174
|
-
onContextMenu={(e) => handleContextMenu(e, node.id)}
|
|
175
243
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
176
|
-
onDragEnd={() => setDraggedId(null)}
|
|
177
|
-
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
178
|
-
|
|
244
|
+
onDragEnd={() => { setDraggedId(null); setDropTarget(null); }}
|
|
245
|
+
onDragOver={(e) => handleDragOver(e, node.id, isRoot)}
|
|
246
|
+
onDragLeave={(e) => handleDragLeave(e, node.id)}
|
|
247
|
+
onDrop={(e) => handleDrop(e, node.id, isRoot)}
|
|
179
248
|
>
|
|
180
249
|
<div style={{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }}>
|
|
181
250
|
<span
|
|
@@ -196,24 +265,104 @@ export default function EditorTree({
|
|
|
196
265
|
</span>
|
|
197
266
|
</div>
|
|
198
267
|
{!isRoot && (
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
268
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
269
|
+
<Dropdown
|
|
270
|
+
placement="bottom-end"
|
|
271
|
+
trigger={({ ref, toggle }) => (
|
|
272
|
+
<button
|
|
273
|
+
ref={ref}
|
|
274
|
+
style={{
|
|
275
|
+
background: 'none',
|
|
276
|
+
border: 'none',
|
|
277
|
+
cursor: 'pointer',
|
|
278
|
+
padding: '0 4px',
|
|
279
|
+
fontSize: 14,
|
|
280
|
+
opacity: 0.7,
|
|
281
|
+
color: 'inherit',
|
|
282
|
+
}}
|
|
283
|
+
onClick={(e) => {
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
toggle();
|
|
286
|
+
}}
|
|
287
|
+
title="Node Actions"
|
|
288
|
+
>
|
|
289
|
+
⋯
|
|
290
|
+
</button>
|
|
291
|
+
)}
|
|
292
|
+
>
|
|
293
|
+
{(close) => (
|
|
294
|
+
<div style={{ ...menu.container, position: 'static' }} onClick={(e) => e.stopPropagation()}>
|
|
295
|
+
<button style={menu.item} onClick={() => { handleAddChild(node.id); close(); }}>
|
|
296
|
+
Add Child
|
|
297
|
+
</button>
|
|
298
|
+
<button style={menu.item} onClick={() => { setSelectedId(node.id); onFocusNode?.(node.id); close(); }}>
|
|
299
|
+
Focus Camera
|
|
300
|
+
</button>
|
|
301
|
+
<button style={menu.item} onClick={() => { handleDuplicate(node.id); close(); }}>
|
|
302
|
+
Duplicate
|
|
303
|
+
</button>
|
|
304
|
+
<button style={{ ...menu.item, ...menu.danger }} onClick={() => { handleDelete(node.id); close(); }}>
|
|
305
|
+
Delete
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</Dropdown>
|
|
310
|
+
<button
|
|
311
|
+
style={{
|
|
312
|
+
background: 'none',
|
|
313
|
+
border: 'none',
|
|
314
|
+
cursor: 'pointer',
|
|
315
|
+
padding: '0 4px',
|
|
316
|
+
fontSize: 14,
|
|
317
|
+
opacity: node.disabled ? 0.5 : 0.7,
|
|
318
|
+
color: 'inherit',
|
|
319
|
+
}}
|
|
320
|
+
onClick={(e) => {
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
handleToggleDisabled(node.id);
|
|
323
|
+
}}
|
|
324
|
+
title={node.disabled ? 'Enable' : 'Disable'}
|
|
325
|
+
>
|
|
326
|
+
{node.disabled ? '◎' : '◉'}
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
{isRoot && (
|
|
331
|
+
<Dropdown
|
|
332
|
+
placement="bottom-end"
|
|
333
|
+
trigger={({ ref, toggle }) => (
|
|
334
|
+
<button
|
|
335
|
+
ref={ref}
|
|
336
|
+
style={{
|
|
337
|
+
background: 'none',
|
|
338
|
+
border: 'none',
|
|
339
|
+
cursor: 'pointer',
|
|
340
|
+
padding: '0 4px',
|
|
341
|
+
fontSize: 14,
|
|
342
|
+
opacity: 0.7,
|
|
343
|
+
color: 'inherit',
|
|
344
|
+
}}
|
|
345
|
+
onClick={(e) => {
|
|
346
|
+
e.stopPropagation();
|
|
347
|
+
toggle();
|
|
348
|
+
}}
|
|
349
|
+
title="Scene Actions"
|
|
350
|
+
>
|
|
351
|
+
⋯
|
|
352
|
+
</button>
|
|
353
|
+
)}
|
|
214
354
|
>
|
|
215
|
-
{
|
|
216
|
-
|
|
355
|
+
{(close) => (
|
|
356
|
+
<div style={{ ...menu.container, position: 'static' }} onClick={(e) => e.stopPropagation()}>
|
|
357
|
+
<button style={menu.item} onClick={() => { handleAddChild(node.id); close(); }}>
|
|
358
|
+
Add Child
|
|
359
|
+
</button>
|
|
360
|
+
<button style={menu.item} onClick={() => { setSelectedId(node.id); onFocusNode?.(node.id); close(); }}>
|
|
361
|
+
Focus Camera
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</Dropdown>
|
|
217
366
|
)}
|
|
218
367
|
</div>
|
|
219
368
|
{!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
|
|
@@ -223,12 +372,7 @@ export default function EditorTree({
|
|
|
223
372
|
|
|
224
373
|
return (
|
|
225
374
|
<>
|
|
226
|
-
<style>
|
|
227
|
-
.tree-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
228
|
-
.tree-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
229
|
-
.tree-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
230
|
-
`}</style>
|
|
231
|
-
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => { setContextMenu(null); setFileMenuOpen(false); }}>
|
|
375
|
+
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }}>
|
|
232
376
|
<div style={base.header}>
|
|
233
377
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
|
|
234
378
|
<span>{collapsed ? '▶' : '▼'}</span>
|
|
@@ -252,28 +396,33 @@ export default function EditorTree({
|
|
|
252
396
|
>
|
|
253
397
|
↷
|
|
254
398
|
</button>
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
399
|
+
<Dropdown
|
|
400
|
+
placement="bottom-end"
|
|
401
|
+
trigger={({ ref, toggle }) => (
|
|
402
|
+
<button
|
|
403
|
+
ref={ref}
|
|
404
|
+
style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
|
|
405
|
+
onClick={(e) => { e.stopPropagation(); toggle(); }}
|
|
406
|
+
title="File"
|
|
407
|
+
>
|
|
408
|
+
⋮
|
|
409
|
+
</button>
|
|
410
|
+
)}
|
|
411
|
+
>
|
|
412
|
+
{(close) => (
|
|
264
413
|
<FileMenu
|
|
265
414
|
prefabData={prefabData}
|
|
266
415
|
setPrefabData={setPrefabData}
|
|
267
|
-
onClose={
|
|
416
|
+
onClose={close}
|
|
268
417
|
/>
|
|
269
418
|
)}
|
|
270
|
-
</
|
|
419
|
+
</Dropdown>
|
|
271
420
|
</div>
|
|
272
421
|
)}
|
|
273
422
|
</div>
|
|
274
423
|
{!collapsed && (
|
|
275
424
|
<>
|
|
276
|
-
<div style={{ padding: '4px 4px', borderBottom:
|
|
425
|
+
<div style={{ padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }}>
|
|
277
426
|
<input
|
|
278
427
|
type="text"
|
|
279
428
|
placeholder="Search nodes..."
|
|
@@ -281,14 +430,8 @@ export default function EditorTree({
|
|
|
281
430
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
282
431
|
onClick={(e) => e.stopPropagation()}
|
|
283
432
|
style={{
|
|
284
|
-
|
|
433
|
+
...base.input,
|
|
285
434
|
padding: '4px 8px',
|
|
286
|
-
background: 'rgba(255,255,255,0.05)',
|
|
287
|
-
border: '1px solid rgba(255,255,255,0.1)',
|
|
288
|
-
borderRadius: 3,
|
|
289
|
-
color: 'inherit',
|
|
290
|
-
fontSize: 11,
|
|
291
|
-
outline: 'none',
|
|
292
435
|
}}
|
|
293
436
|
/>
|
|
294
437
|
</div>
|
|
@@ -297,27 +440,6 @@ export default function EditorTree({
|
|
|
297
440
|
)}
|
|
298
441
|
</div>
|
|
299
442
|
|
|
300
|
-
{contextMenu && (
|
|
301
|
-
<div
|
|
302
|
-
style={{ ...menu.container, top: contextMenu.y, left: contextMenu.x }}
|
|
303
|
-
onClick={(e) => e.stopPropagation()}
|
|
304
|
-
onPointerLeave={() => setContextMenu(null)}
|
|
305
|
-
>
|
|
306
|
-
<button style={menu.item} onClick={() => handleAddChild(contextMenu.nodeId)}>
|
|
307
|
-
Add Child
|
|
308
|
-
</button>
|
|
309
|
-
{contextMenu.nodeId !== prefabData.root.id && (
|
|
310
|
-
<>
|
|
311
|
-
<button style={menu.item} onClick={() => handleDuplicate(contextMenu.nodeId)}>
|
|
312
|
-
Duplicate
|
|
313
|
-
</button>
|
|
314
|
-
<button style={{ ...menu.item, ...menu.danger }} onClick={() => handleDelete(contextMenu.nodeId)}>
|
|
315
|
-
Delete
|
|
316
|
-
</button>
|
|
317
|
-
</>
|
|
318
|
-
)}
|
|
319
|
-
</div>
|
|
320
|
-
)}
|
|
321
443
|
</>
|
|
322
444
|
);
|
|
323
445
|
}
|
|
@@ -361,7 +483,7 @@ function FileMenu({
|
|
|
361
483
|
|
|
362
484
|
return (
|
|
363
485
|
<div
|
|
364
|
-
style={{ ...menu.container,
|
|
486
|
+
style={{ ...menu.container, position: 'static' }}
|
|
365
487
|
onClick={(e) => e.stopPropagation()}
|
|
366
488
|
>
|
|
367
489
|
<button
|
|
@@ -2,7 +2,7 @@ import { Dispatch, SetStateAction, useState, useEffect } from 'react';
|
|
|
2
2
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
3
3
|
import EditorTree from './EditorTree';
|
|
4
4
|
import { getAllComponents } from './components/ComponentRegistry';
|
|
5
|
-
import { base, inspector } from './styles';
|
|
5
|
+
import { base, colors, inspector, scrollbarCSS, componentCard } from './styles';
|
|
6
6
|
import { findNode, updateNode, deleteNode } from './utils';
|
|
7
7
|
|
|
8
8
|
function EditorUI({
|
|
@@ -45,12 +45,7 @@ function EditorUI({
|
|
|
45
45
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
46
46
|
|
|
47
47
|
return <>
|
|
48
|
-
<style>{
|
|
49
|
-
.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
50
|
-
.prefab-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
51
|
-
.prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
52
|
-
.prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
|
|
53
|
-
`}</style>
|
|
48
|
+
<style>{scrollbarCSS}</style>
|
|
54
49
|
<div style={inspector.panel}>
|
|
55
50
|
<div style={base.header} onClick={() => setCollapsed(!collapsed)}>
|
|
56
51
|
<span>Inspector</span>
|
|
@@ -102,11 +97,11 @@ function NodeInspector({
|
|
|
102
97
|
if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
|
|
103
98
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
104
99
|
|
|
105
|
-
return <div style={
|
|
100
|
+
return <div style={inspector.content} className="prefab-scroll">
|
|
106
101
|
{/* Node Name */}
|
|
107
102
|
<div style={base.section}>
|
|
108
103
|
<div style={{ display: "flex", marginBottom: 8, alignItems: 'center', gap: 8 }}>
|
|
109
|
-
<div style={{ fontSize: 10, color:
|
|
104
|
+
<div style={{ fontSize: 10, color: colors.textDim, wordBreak: 'break-all', border: `1px solid ${colors.border}`, padding: '2px 6px', borderRadius: 3, flex: 1, fontFamily: 'monospace' }}>
|
|
110
105
|
{node.id}
|
|
111
106
|
</div>
|
|
112
107
|
<button style={{ ...base.btn, ...base.btnDanger }} title="Delete Node" onClick={deleteNode}>❌</button>
|
|
@@ -131,12 +126,12 @@ function NodeInspector({
|
|
|
131
126
|
{node.components && Object.entries(node.components).map(([key, comp]: [string, any]) => {
|
|
132
127
|
if (!comp) return null;
|
|
133
128
|
const def = ALL_COMPONENTS[comp.type];
|
|
134
|
-
if (!def) return <div key={key} style={{ color:
|
|
129
|
+
if (!def) return <div key={key} style={{ color: colors.danger, fontSize: 11 }}>
|
|
135
130
|
Unknown: {comp.type}
|
|
136
131
|
</div>;
|
|
137
132
|
|
|
138
133
|
return (
|
|
139
|
-
<div key={key} style={
|
|
134
|
+
<div key={key} style={componentCard.container}>
|
|
140
135
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
|
141
136
|
<div style={{ fontSize: 11, fontWeight: 500 }}>{key}</div>
|
|
142
137
|
<button
|