react-three-game 0.0.57 → 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/tools/prefabeditor/EditorTree.js +24 -48
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +32 -12
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +49 -23
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +11 -5
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +34 -13
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/tools/prefabeditor/EditorTree.tsx +56 -125
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/components/CameraComponent.tsx +51 -14
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +59 -28
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +18 -9
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +49 -18
package/README.md
CHANGED
|
@@ -60,14 +60,23 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
|
|
|
60
60
|
## GameObject Schema
|
|
61
61
|
|
|
62
62
|
```typescript
|
|
63
|
+
interface Prefab {
|
|
64
|
+
id?: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
root: GameObject;
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
interface GameObject {
|
|
64
70
|
id: string;
|
|
71
|
+
name?: string;
|
|
65
72
|
disabled?: boolean;
|
|
66
73
|
components?: Record<string, { type: string; properties: any }>;
|
|
67
74
|
children?: GameObject[];
|
|
68
75
|
}
|
|
69
76
|
```
|
|
70
77
|
|
|
78
|
+
`disabled` is the canonical visibility toggle in the current schema. Transforms are local to the parent node.
|
|
79
|
+
|
|
71
80
|
## Built-in Components
|
|
72
81
|
|
|
73
82
|
| Component | Key Properties |
|
|
@@ -110,7 +119,7 @@ const Rotator: Component = {
|
|
|
110
119
|
registerComponent(Rotator); // before rendering PrefabEditor
|
|
111
120
|
```
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
Components may render visible content, wrap child content, or contribute runtime behavior. Keep those semantics explicit in the component `View` rather than relying on hidden tree rules.
|
|
114
123
|
|
|
115
124
|
### Schema-Driven Field Types
|
|
116
125
|
|
|
@@ -152,12 +161,16 @@ import { PrefabEditor } from 'react-three-game';
|
|
|
152
161
|
</PrefabEditor>
|
|
153
162
|
```
|
|
154
163
|
|
|
155
|
-
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent.
|
|
164
|
+
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
165
|
+
|
|
166
|
+
Editor menu structure:
|
|
167
|
+
- `Menu > File`: new scene, load/save prefab JSON, load prefab into scene
|
|
168
|
+
- `Menu > Export`: `GLB`, `PNG`
|
|
156
169
|
|
|
157
170
|
## Internals
|
|
158
171
|
|
|
159
172
|
- **Transforms**: Local in JSON, world computed via matrix multiplication
|
|
160
|
-
- **Instancing**: `model.properties.instanced = true`
|
|
173
|
+
- **Instancing**: `model.properties.instanced = true` switches the node to the batched instance path (`<Merged>` / `<InstancedRigidBodies>`) instead of the standard model render path
|
|
161
174
|
- **Models**: GLB/GLTF (Draco) and FBX auto-load from `filename`
|
|
162
175
|
|
|
163
176
|
## Tree Utilities
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
11
2
|
import { useState } from 'react';
|
|
12
3
|
import { getComponent } from './components/ComponentRegistry';
|
|
13
|
-
import { base, colors, tree
|
|
14
|
-
import { findNode, findParent, deleteNode, cloneNode, updateNodeById
|
|
4
|
+
import { base, colors, tree } from './styles';
|
|
5
|
+
import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
|
|
15
6
|
import { useEditorContext } from './EditorContext';
|
|
16
7
|
import { Dropdown } from './Dropdown';
|
|
8
|
+
import { FileMenu, MenuTriggerButton, TreeContextMenu, TreeNodeMenu } from './EditorTreeMenus';
|
|
17
9
|
function moveNode(root, draggedId, targetId, position) {
|
|
18
10
|
const draggedNode = findNode(root, draggedId);
|
|
19
11
|
const oldParent = findParent(root, draggedId);
|
|
@@ -79,6 +71,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
79
71
|
const [collapsedIds, setCollapsedIds] = useState(new Set());
|
|
80
72
|
const [collapsed, setCollapsed] = useState(false);
|
|
81
73
|
const [searchQuery, setSearchQuery] = useState('');
|
|
74
|
+
const [contextMenu, setContextMenu] = useState(null);
|
|
82
75
|
if (!prefabData || !setPrefabData)
|
|
83
76
|
return null;
|
|
84
77
|
const toggleCollapse = (e, id) => {
|
|
@@ -128,6 +121,16 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
128
121
|
const handleToggleDisabled = (nodeId) => {
|
|
129
122
|
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, nodeId, node => (Object.assign(Object.assign({}, node), { disabled: !node.disabled }))) })));
|
|
130
123
|
};
|
|
124
|
+
const closeContextMenu = () => setContextMenu(null);
|
|
125
|
+
const openContextMenu = (nodeId, x, y) => {
|
|
126
|
+
setSelectedId(nodeId);
|
|
127
|
+
setContextMenu({ nodeId, x, y });
|
|
128
|
+
};
|
|
129
|
+
const handleFocus = (nodeId) => {
|
|
130
|
+
setSelectedId(nodeId);
|
|
131
|
+
onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(nodeId);
|
|
132
|
+
};
|
|
133
|
+
const renderTreeNodeMenu = (nodeId, isRoot, onClose) => (_jsx(TreeNodeMenu, { isRoot: isRoot, nodeId: nodeId, onAddChild: handleAddChild, onFocus: handleFocus, onDuplicate: isRoot ? undefined : handleDuplicate, onDelete: isRoot ? undefined : handleDelete, onClose: onClose }));
|
|
131
134
|
const handleDragStart = (e, id) => {
|
|
132
135
|
if (id === prefabData.root.id)
|
|
133
136
|
return e.preventDefault();
|
|
@@ -190,13 +193,17 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
190
193
|
const isDropTarget = (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.id) === node.id;
|
|
191
194
|
const showDropBefore = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'before';
|
|
192
195
|
const showDropInside = isDropTarget && (dropTarget === null || dropTarget === void 0 ? void 0 : dropTarget.position) === 'inside';
|
|
193
|
-
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined, boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); },
|
|
196
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: Object.assign(Object.assign(Object.assign({}, tree.row), (isSelected ? tree.selected : {})), { paddingLeft: `${depth * 12 + 6}px`, opacity: node.disabled ? 0.4 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined, boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined }), draggable: !isRoot, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
openContextMenu(node.id, e.clientX, e.clientY);
|
|
200
|
+
}, onDragStart: (e) => handleDragStart(e, node.id), onDragEnd: () => { setDraggedId(null); setDropTarget(null); }, onDragOver: (e) => handleDragOver(e, node.id, isRoot), onDragLeave: (e) => handleDragLeave(e, node.id), onDrop: (e) => handleDrop(e, node.id, isRoot), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }, children: [_jsx("span", { style: {
|
|
194
201
|
width: 12,
|
|
195
202
|
opacity: 0.6,
|
|
196
203
|
marginRight: 4,
|
|
197
204
|
cursor: 'pointer',
|
|
198
205
|
visibility: hasChildren ? 'visible' : 'hidden'
|
|
199
|
-
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isRoot && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(
|
|
206
|
+
}, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), !isRoot && _jsx("span", { style: { marginRight: 4, opacity: 0.4 }, children: "\u22EE\u22EE" }), _jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = node.name) !== null && _a !== void 0 ? _a : node.id })] }), !isRoot && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Node Actions", style: {
|
|
200
207
|
background: 'none',
|
|
201
208
|
border: 'none',
|
|
202
209
|
cursor: 'pointer',
|
|
@@ -204,10 +211,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
204
211
|
fontSize: 14,
|
|
205
212
|
opacity: 0.7,
|
|
206
213
|
color: 'inherit',
|
|
207
|
-
},
|
|
208
|
-
e.stopPropagation();
|
|
209
|
-
toggle();
|
|
210
|
-
}, title: "Node Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" }), _jsx("button", { style: menu.item, onClick: () => { handleDuplicate(node.id); close(); }, children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => { handleDelete(node.id); close(); }, children: "Delete" })] })) }), _jsx("button", { style: {
|
|
214
|
+
}, children: "\u22EF" })), children: (close) => renderTreeNodeMenu(node.id, false, close) }), _jsx("button", { style: {
|
|
211
215
|
background: 'none',
|
|
212
216
|
border: 'none',
|
|
213
217
|
cursor: 'pointer',
|
|
@@ -218,7 +222,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
218
222
|
}, onClick: (e) => {
|
|
219
223
|
e.stopPropagation();
|
|
220
224
|
handleToggleDisabled(node.id);
|
|
221
|
-
}, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' })] })), isRoot && (_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(
|
|
225
|
+
}, title: node.disabled ? 'Enable' : 'Disable', children: node.disabled ? '◎' : '◉' })] })), isRoot && (_jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Scene Actions", style: {
|
|
222
226
|
background: 'none',
|
|
223
227
|
border: 'none',
|
|
224
228
|
cursor: 'pointer',
|
|
@@ -226,35 +230,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
226
230
|
fontSize: 14,
|
|
227
231
|
opacity: 0.7,
|
|
228
232
|
color: 'inherit',
|
|
229
|
-
},
|
|
230
|
-
e.stopPropagation();
|
|
231
|
-
toggle();
|
|
232
|
-
}, title: "Scene Actions", children: "\u22EF" })), children: (close) => (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { handleAddChild(node.id); close(); }, children: "Add Child" }), _jsx("button", { style: menu.item, onClick: () => { setSelectedId(node.id); onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(node.id); close(); }, children: "Focus Camera" })] })) }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
233
|
+
}, children: "\u22EF" })), children: (close) => renderTreeNodeMenu(node.id, true, close) }))] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
|
|
233
234
|
};
|
|
234
|
-
return (
|
|
235
|
-
}
|
|
236
|
-
function FileMenu({ prefabData, setPrefabData, onClose }) {
|
|
237
|
-
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
238
|
-
const handleLoad = () => __awaiter(this, void 0, void 0, function* () {
|
|
239
|
-
const loadedPrefab = yield loadJson();
|
|
240
|
-
if (!loadedPrefab)
|
|
241
|
-
return;
|
|
242
|
-
setPrefabData(loadedPrefab);
|
|
243
|
-
onClose();
|
|
244
|
-
});
|
|
245
|
-
const handleSave = () => {
|
|
246
|
-
saveJson(prefabData, "prefab");
|
|
247
|
-
onClose();
|
|
248
|
-
};
|
|
249
|
-
const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
|
|
250
|
-
const loadedPrefab = yield loadJson();
|
|
251
|
-
if (!loadedPrefab)
|
|
252
|
-
return;
|
|
253
|
-
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
|
|
254
|
-
var _a;
|
|
255
|
-
return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
|
|
256
|
-
}) })));
|
|
257
|
-
onClose();
|
|
258
|
-
});
|
|
259
|
-
return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { position: 'static' }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: handleLoad, children: "\uD83D\uDCE5 Load Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleSave, children: "\uD83D\uDCBE Save Prefab JSON" }), _jsx("button", { style: menu.item, onClick: handleLoadIntoScene, children: "\uD83D\uDCC2 Load into Scene" }), _jsx("button", { style: menu.item, onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "\uD83D\uDCF8 Screenshot" }), _jsx("button", { style: menu.item, onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "\uD83D\uDCE6 Export GLB" })] }));
|
|
235
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), children: [_jsxs("div", { style: base.header, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: collapsed ? '▶' : '▼' }), _jsx("span", { children: "Scene" })] }), !collapsed && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canUndo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onUndo === null || onUndo === void 0 ? void 0 : onUndo(); }, disabled: !canUndo, title: "Undo", children: "\u21B6" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10, opacity: canRedo ? 1 : 0.4 }), onClick: (e) => { e.stopPropagation(); onRedo === null || onRedo === void 0 ? void 0 : onRedo(); }, disabled: !canRedo, title: "Redo", children: "\u21B7" }), _jsx(Dropdown, { placement: "bottom-end", trigger: ({ ref, toggle }) => (_jsx(MenuTriggerButton, { buttonRef: ref, onToggle: toggle, title: "Menu", style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), children: "\u22EE" })), children: (close) => (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: close })) })] }))] }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { style: { padding: '4px 4px', borderBottom: `1px solid ${colors.borderLight}` }, children: _jsx("input", { type: "text", placeholder: "Search nodes...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), style: Object.assign(Object.assign({}, base.input), { padding: '4px 8px' }) }) }), _jsx("div", { className: "tree-scroll", style: tree.scroll, children: renderNode(prefabData.root) })] }))] }), _jsx(TreeContextMenu, { contextMenu: contextMenu, onClose: closeContextMenu, children: (nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === prefabData.root.id, close) })] }));
|
|
260
236
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { Prefab } from './types';
|
|
3
|
+
export type TreeContextMenuState = {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
} | null;
|
|
8
|
+
export declare function MenuTriggerButton({ buttonRef, onToggle, title, style, children, }: {
|
|
9
|
+
buttonRef: React.RefObject<HTMLButtonElement | null>;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
title: string;
|
|
12
|
+
style: React.CSSProperties;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }: {
|
|
16
|
+
isRoot: boolean;
|
|
17
|
+
nodeId: string;
|
|
18
|
+
onAddChild: (parentId: string) => void;
|
|
19
|
+
onFocus: (nodeId: string) => void;
|
|
20
|
+
onDuplicate?: (nodeId: string) => void;
|
|
21
|
+
onDelete?: (nodeId: string) => void;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function TreeContextMenu({ contextMenu, onClose, children, }: {
|
|
25
|
+
contextMenu: TreeContextMenuState;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
children: (nodeId: string, onClose: () => void) => React.ReactNode;
|
|
28
|
+
}): import("react").ReactPortal | null;
|
|
29
|
+
export declare function FileMenu({ prefabData, setPrefabData, onClose }: {
|
|
30
|
+
prefabData: Prefab;
|
|
31
|
+
setPrefabData: Dispatch<SetStateAction<Prefab>>;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { useEffect, useRef, useState } from 'react';
|
|
12
|
+
import { createPortal } from 'react-dom';
|
|
13
|
+
import { menu } from './styles';
|
|
14
|
+
import { useEditorContext } from './EditorContext';
|
|
15
|
+
import { getComponent } from './components/ComponentRegistry';
|
|
16
|
+
import { loadJson, saveJson, regenerateIds, updateNodeById } from './utils';
|
|
17
|
+
function createEmptyPrefab() {
|
|
18
|
+
var _a;
|
|
19
|
+
return {
|
|
20
|
+
id: crypto.randomUUID(),
|
|
21
|
+
name: 'New Scene',
|
|
22
|
+
root: {
|
|
23
|
+
id: crypto.randomUUID(),
|
|
24
|
+
name: 'Scene',
|
|
25
|
+
components: {
|
|
26
|
+
transform: {
|
|
27
|
+
type: 'Transform',
|
|
28
|
+
properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
children: []
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function MenuPanel({ children, style, }) {
|
|
36
|
+
return (_jsx("div", { style: Object.assign(Object.assign(Object.assign({}, menu.container), { position: 'static' }), style), onClick: (e) => e.stopPropagation(), children: children }));
|
|
37
|
+
}
|
|
38
|
+
function MenuItemButton({ children, onClick, danger = false, style, }) {
|
|
39
|
+
return (_jsx("button", { style: danger ? Object.assign(Object.assign(Object.assign({}, menu.item), menu.danger), style) : Object.assign(Object.assign({}, menu.item), style), onClick: onClick, children: children }));
|
|
40
|
+
}
|
|
41
|
+
function MenuSubmenu({ label, children, }) {
|
|
42
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
43
|
+
return (_jsxs("div", { style: { position: 'relative' }, onMouseEnter: () => setIsOpen(true), onMouseLeave: () => setIsOpen(false), children: [_jsxs(MenuItemButton, { onClick: () => setIsOpen(open => !open), style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }, children: [_jsx("span", { children: label }), _jsx("span", { "aria-hidden": "true", children: "\u203A" })] }), isOpen && (_jsx("div", { style: {
|
|
44
|
+
position: 'absolute',
|
|
45
|
+
top: 0,
|
|
46
|
+
left: '100%',
|
|
47
|
+
zIndex: 1,
|
|
48
|
+
}, children: _jsx(MenuPanel, { children: children }) }))] }));
|
|
49
|
+
}
|
|
50
|
+
export function MenuTriggerButton({ buttonRef, onToggle, title, style, children, }) {
|
|
51
|
+
return (_jsx("button", { ref: buttonRef, style: style, onClick: (e) => {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
onToggle();
|
|
54
|
+
}, title: title, children: children }));
|
|
55
|
+
}
|
|
56
|
+
export function TreeNodeMenu({ isRoot, nodeId, onAddChild, onFocus, onDuplicate, onDelete, onClose, }) {
|
|
57
|
+
return (_jsxs(MenuPanel, { children: [_jsx(MenuItemButton, { onClick: () => { onAddChild(nodeId); onClose(); }, children: "Add Child" }), _jsx(MenuItemButton, { onClick: () => { onFocus(nodeId); onClose(); }, children: "Focus Camera" }), !isRoot && onDuplicate && (_jsx(MenuItemButton, { onClick: () => { onDuplicate(nodeId); onClose(); }, children: "Duplicate" })), !isRoot && onDelete && (_jsx(MenuItemButton, { danger: true, onClick: () => { onDelete(nodeId); onClose(); }, children: "Delete" }))] }));
|
|
58
|
+
}
|
|
59
|
+
export function TreeContextMenu({ contextMenu, onClose, children, }) {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
const panelRef = useRef(null);
|
|
62
|
+
const [position, setPosition] = useState(null);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!contextMenu)
|
|
65
|
+
return;
|
|
66
|
+
const handlePointerDown = (event) => {
|
|
67
|
+
var _a;
|
|
68
|
+
const target = event.target;
|
|
69
|
+
if (!target)
|
|
70
|
+
return;
|
|
71
|
+
if ((_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))
|
|
72
|
+
return;
|
|
73
|
+
onClose();
|
|
74
|
+
};
|
|
75
|
+
const handleKeyDown = (event) => {
|
|
76
|
+
if (event.key === 'Escape')
|
|
77
|
+
onClose();
|
|
78
|
+
};
|
|
79
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
80
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
81
|
+
return () => {
|
|
82
|
+
document.removeEventListener('pointerdown', handlePointerDown);
|
|
83
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
84
|
+
};
|
|
85
|
+
}, [contextMenu, onClose]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!contextMenu || !panelRef.current || typeof window === 'undefined')
|
|
88
|
+
return;
|
|
89
|
+
const panelRect = panelRef.current.getBoundingClientRect();
|
|
90
|
+
const left = Math.max(8, Math.min(contextMenu.x, window.innerWidth - panelRect.width - 8));
|
|
91
|
+
const top = Math.max(8, Math.min(contextMenu.y, window.innerHeight - panelRect.height - 8));
|
|
92
|
+
setPosition({ left, top });
|
|
93
|
+
}, [contextMenu]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!contextMenu) {
|
|
96
|
+
setPosition(null);
|
|
97
|
+
}
|
|
98
|
+
}, [contextMenu]);
|
|
99
|
+
if (!contextMenu || typeof document === 'undefined')
|
|
100
|
+
return null;
|
|
101
|
+
return createPortal(_jsx("div", { ref: panelRef, style: {
|
|
102
|
+
position: 'fixed',
|
|
103
|
+
left: (_a = position === null || position === void 0 ? void 0 : position.left) !== null && _a !== void 0 ? _a : contextMenu.x,
|
|
104
|
+
top: (_b = position === null || position === void 0 ? void 0 : position.top) !== null && _b !== void 0 ? _b : contextMenu.y,
|
|
105
|
+
zIndex: 1000,
|
|
106
|
+
}, onMouseLeave: onClose, onContextMenu: (e) => e.preventDefault(), children: children(contextMenu.nodeId, onClose) }), document.body);
|
|
107
|
+
}
|
|
108
|
+
export function FileMenu({ prefabData, setPrefabData, onClose }) {
|
|
109
|
+
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
110
|
+
const handleNewScene = () => {
|
|
111
|
+
setPrefabData(createEmptyPrefab());
|
|
112
|
+
onClose();
|
|
113
|
+
};
|
|
114
|
+
const handleNewSceneFromPrefab = () => __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
const loadedPrefab = yield loadJson();
|
|
116
|
+
if (!loadedPrefab)
|
|
117
|
+
return;
|
|
118
|
+
setPrefabData(loadedPrefab);
|
|
119
|
+
onClose();
|
|
120
|
+
});
|
|
121
|
+
const handleSave = () => {
|
|
122
|
+
saveJson(prefabData, 'prefab');
|
|
123
|
+
onClose();
|
|
124
|
+
};
|
|
125
|
+
const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
|
|
126
|
+
const loadedPrefab = yield loadJson();
|
|
127
|
+
if (!loadedPrefab)
|
|
128
|
+
return;
|
|
129
|
+
setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
|
|
130
|
+
var _a;
|
|
131
|
+
return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
|
|
132
|
+
}) })));
|
|
133
|
+
onClose();
|
|
134
|
+
});
|
|
135
|
+
return (_jsxs(MenuPanel, { style: { overflow: 'visible' }, children: [_jsxs(MenuSubmenu, { label: "File", children: [_jsx(MenuItemButton, { onClick: handleNewScene, children: "New Scene" }), _jsx(MenuItemButton, { onClick: handleNewSceneFromPrefab, children: "New Scene from Prefab" }), _jsx(MenuItemButton, { onClick: handleLoadIntoScene, children: "Load Prefab into Scene" }), _jsx(MenuItemButton, { onClick: handleSave, children: "Save Prefab" })] }), _jsxs(MenuSubmenu, { label: "Export", children: [_jsx(MenuItemButton, { onClick: () => { onExportGLB === null || onExportGLB === void 0 ? void 0 : onExportGLB(); onClose(); }, children: "GLB" }), _jsx(MenuItemButton, { onClick: () => { onScreenshot === null || onScreenshot === void 0 ? void 0 : onScreenshot(); onClose(); }, children: "PNG" })] })] }));
|
|
136
|
+
}
|
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { PerspectiveCamera
|
|
3
|
-
import {
|
|
2
|
+
import { PerspectiveCamera } from '@react-three/drei';
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { CameraHelper } from 'three';
|
|
5
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
6
|
import { FieldGroup, NumberField } from './Input';
|
|
7
|
+
const cameraDefaults = {
|
|
8
|
+
fov: 50,
|
|
9
|
+
near: 0.1,
|
|
10
|
+
zoom: 1,
|
|
11
|
+
far: 1000,
|
|
12
|
+
};
|
|
6
13
|
function CameraComponentEditor({ component, onUpdate }) {
|
|
7
|
-
|
|
14
|
+
const values = Object.assign(Object.assign({}, cameraDefaults), component.properties);
|
|
15
|
+
return (_jsxs(FieldGroup, { children: [_jsx(NumberField, { name: "fov", label: "FOV", values: values, onChange: onUpdate, fallback: 50, min: 1, max: 179, step: 1 }), _jsx(NumberField, { name: "near", label: "Near", values: values, onChange: onUpdate, fallback: 0.1, min: 0.001, step: 0.1 }), _jsx(NumberField, { name: "zoom", label: "Zoom", values: values, onChange: onUpdate, fallback: 1, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "far", label: "Far", values: values, onChange: onUpdate, fallback: 1000, min: 0.1, step: 1 })] }));
|
|
8
16
|
}
|
|
9
17
|
function CameraComponentView({ properties, editMode, isSelected }) {
|
|
10
|
-
|
|
11
|
-
const fov =
|
|
12
|
-
const near =
|
|
13
|
-
const zoom =
|
|
14
|
-
const far =
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
const merged = Object.assign(Object.assign({}, cameraDefaults), properties);
|
|
19
|
+
const fov = merged.fov;
|
|
20
|
+
const near = merged.near;
|
|
21
|
+
const zoom = merged.zoom;
|
|
22
|
+
const far = merged.far;
|
|
23
|
+
const [camera, setCamera] = useState(null);
|
|
24
|
+
const cameraHelper = useMemo(() => camera ? new CameraHelper(camera) : null, [camera]);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => {
|
|
27
|
+
cameraHelper === null || cameraHelper === void 0 ? void 0 : cameraHelper.dispose();
|
|
28
|
+
};
|
|
29
|
+
}, [cameraHelper]);
|
|
30
|
+
useFrame(() => {
|
|
31
|
+
if (camera && cameraHelper && editMode && isSelected) {
|
|
32
|
+
camera.updateProjectionMatrix();
|
|
33
|
+
camera.updateMatrixWorld();
|
|
34
|
+
cameraHelper.update();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: (instance) => setCamera(instance), makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode && isSelected && cameraHelper && (_jsx("primitive", { object: cameraHelper })), editMode && !isSelected ? (_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.34, 0.22, 0.18] }), _jsx("meshBasicMaterial", { color: "#22d3ee", wireframe: true })] })) : null] }));
|
|
18
38
|
}
|
|
19
39
|
const CameraComponent = {
|
|
20
40
|
name: 'Camera',
|
|
21
41
|
Editor: CameraComponentEditor,
|
|
22
42
|
View: CameraComponentView,
|
|
23
|
-
defaultProperties:
|
|
43
|
+
defaultProperties: cameraDefaults,
|
|
24
44
|
};
|
|
25
45
|
export default CameraComponent;
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
2
|
+
import { useRef, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
|
-
import { Vector3 } from "three";
|
|
4
|
+
import { CameraHelper, Vector3 } from "three";
|
|
5
5
|
import { FieldRenderer, Input } from "./Input";
|
|
6
6
|
const smallLabel = { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 };
|
|
7
|
+
const directionalLightDefaults = {
|
|
8
|
+
color: '#ffffff',
|
|
9
|
+
intensity: 1,
|
|
10
|
+
castShadow: true,
|
|
11
|
+
shadowMapSize: 1024,
|
|
12
|
+
shadowCameraNear: 0.1,
|
|
13
|
+
shadowCameraFar: 100,
|
|
14
|
+
shadowCameraTop: 30,
|
|
15
|
+
shadowCameraBottom: -30,
|
|
16
|
+
shadowCameraLeft: -30,
|
|
17
|
+
shadowCameraRight: 30,
|
|
18
|
+
targetOffset: [0, -5, 0],
|
|
19
|
+
};
|
|
7
20
|
const directionalLightFields = [
|
|
8
21
|
{ name: 'color', type: 'color', label: 'Color' },
|
|
9
22
|
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
@@ -29,39 +42,52 @@ const directionalLightFields = [
|
|
|
29
42
|
},
|
|
30
43
|
];
|
|
31
44
|
function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
32
|
-
|
|
45
|
+
const values = Object.assign(Object.assign({}, directionalLightDefaults), component.properties);
|
|
46
|
+
const fields = values.castShadow
|
|
47
|
+
? directionalLightFields
|
|
48
|
+
: directionalLightFields.filter(field => field.name !== '_shadowCamera');
|
|
49
|
+
return (_jsx(FieldRenderer, { fields: fields, values: values, onChange: onUpdate }));
|
|
33
50
|
}
|
|
34
51
|
function DirectionalLightView({ properties, editMode, isSelected }) {
|
|
35
|
-
|
|
36
|
-
const color =
|
|
37
|
-
const intensity =
|
|
38
|
-
const castShadow =
|
|
39
|
-
const shadowMapSize =
|
|
40
|
-
const shadowCameraNear =
|
|
41
|
-
const shadowCameraFar =
|
|
42
|
-
const shadowCameraTop =
|
|
43
|
-
const shadowCameraBottom =
|
|
44
|
-
const shadowCameraLeft =
|
|
45
|
-
const shadowCameraRight =
|
|
46
|
-
const targetOffset =
|
|
52
|
+
const merged = Object.assign(Object.assign({}, directionalLightDefaults), properties);
|
|
53
|
+
const color = merged.color;
|
|
54
|
+
const intensity = merged.intensity;
|
|
55
|
+
const castShadow = merged.castShadow;
|
|
56
|
+
const shadowMapSize = merged.shadowMapSize;
|
|
57
|
+
const shadowCameraNear = merged.shadowCameraNear;
|
|
58
|
+
const shadowCameraFar = merged.shadowCameraFar;
|
|
59
|
+
const shadowCameraTop = merged.shadowCameraTop;
|
|
60
|
+
const shadowCameraBottom = merged.shadowCameraBottom;
|
|
61
|
+
const shadowCameraLeft = merged.shadowCameraLeft;
|
|
62
|
+
const shadowCameraRight = merged.shadowCameraRight;
|
|
63
|
+
const targetOffset = merged.targetOffset;
|
|
47
64
|
const directionalLightRef = useRef(null);
|
|
48
65
|
const targetRef = useRef(null);
|
|
49
|
-
|
|
66
|
+
const [shadowCamera, setShadowCamera] = useState(null);
|
|
67
|
+
const shadowCameraHelper = useMemo(() => shadowCamera ? new CameraHelper(shadowCamera) : null, [shadowCamera]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return () => {
|
|
70
|
+
shadowCameraHelper === null || shadowCameraHelper === void 0 ? void 0 : shadowCameraHelper.dispose();
|
|
71
|
+
};
|
|
72
|
+
}, [shadowCameraHelper]);
|
|
73
|
+
// Use a local target object so node transforms rotate the light direction naturally.
|
|
50
74
|
useEffect(() => {
|
|
51
75
|
if (directionalLightRef.current && targetRef.current) {
|
|
52
76
|
directionalLightRef.current.target = targetRef.current;
|
|
77
|
+
setShadowCamera(directionalLightRef.current.shadow.camera);
|
|
53
78
|
}
|
|
54
79
|
}, []);
|
|
55
|
-
// Update target world position based on light position + offset
|
|
56
80
|
useFrame(() => {
|
|
57
81
|
if (!directionalLightRef.current || !targetRef.current)
|
|
58
82
|
return;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
directionalLightRef.current.target.updateMatrixWorld();
|
|
84
|
+
if (shadowCamera && shadowCameraHelper && castShadow) {
|
|
85
|
+
shadowCamera.updateProjectionMatrix();
|
|
86
|
+
shadowCamera.updateMatrixWorld();
|
|
87
|
+
shadowCameraHelper.update();
|
|
88
|
+
}
|
|
63
89
|
});
|
|
64
|
-
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef }), editMode && isSelected && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
90
|
+
return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": -0.001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: targetOffset }), editMode && isSelected && castShadow && shadowCameraHelper && (_jsx("primitive", { object: shadowCameraHelper })), editMode && isSelected && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
|
|
65
91
|
const points = [
|
|
66
92
|
new Vector3(0, 0, 0),
|
|
67
93
|
new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
|
|
@@ -73,6 +99,6 @@ const DirectionalLightComponent = {
|
|
|
73
99
|
name: 'DirectionalLight',
|
|
74
100
|
Editor: DirectionalLightComponentEditor,
|
|
75
101
|
View: DirectionalLightView,
|
|
76
|
-
defaultProperties:
|
|
102
|
+
defaultProperties: directionalLightDefaults
|
|
77
103
|
};
|
|
78
104
|
export default DirectionalLightComponent;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import type { ThreeElement } from '@react-three/fiber';
|
|
1
2
|
import { Component } from './ComponentRegistry';
|
|
3
|
+
import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
2
4
|
import { MeshBasicMaterialProperties, MeshStandardMaterialProperties } from 'three';
|
|
5
|
+
declare module '@react-three/fiber' {
|
|
6
|
+
interface ThreeElements {
|
|
7
|
+
meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>;
|
|
8
|
+
meshStandardNodeMaterial: ThreeElement<typeof MeshStandardNodeMaterial>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
export interface MaterialProps extends Omit<MeshStandardMaterialProperties & MeshBasicMaterialProperties, 'args' | 'normalScale'> {
|
|
4
12
|
materialType?: 'standard' | 'basic';
|
|
5
13
|
transmission?: number;
|
|
@@ -11,14 +11,20 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
14
|
+
import { extend } from '@react-three/fiber';
|
|
14
15
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
15
16
|
import { createPortal } from 'react-dom';
|
|
16
17
|
import { FieldRenderer, Input } from './Input';
|
|
17
18
|
import { colors } from '../styles';
|
|
18
19
|
import { useMemo } from 'react';
|
|
20
|
+
import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
19
21
|
import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
|
|
20
22
|
const PICKER_POPUP_WIDTH = 260;
|
|
21
23
|
const PICKER_POPUP_HEIGHT = 360;
|
|
24
|
+
extend({
|
|
25
|
+
MeshBasicNodeMaterial,
|
|
26
|
+
MeshStandardNodeMaterial,
|
|
27
|
+
});
|
|
22
28
|
function TexturePicker({ value, onChange, basePath }) {
|
|
23
29
|
const [textureFiles, setTextureFiles] = useState([]);
|
|
24
30
|
const [showPicker, setShowPicker] = useState(false);
|
|
@@ -193,7 +199,7 @@ function MaterialComponentView({ properties, loadedTextures }) {
|
|
|
193
199
|
const normalMapTexture = normalMapTextureName && loadedTextures ? loadedTextures[normalMapTextureName] : undefined;
|
|
194
200
|
const materialSource = properties !== null && properties !== void 0 ? properties : {};
|
|
195
201
|
// Destructure all material props and separate custom texture handling props
|
|
196
|
-
const { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, side: sideProp
|
|
202
|
+
const { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side"]);
|
|
197
203
|
const sideMap = { FrontSide, BackSide, DoubleSide };
|
|
198
204
|
const resolvedSide = sideProp ? ((_b = sideMap[sideProp]) !== null && _b !== void 0 ? _b : FrontSide) : FrontSide;
|
|
199
205
|
const minFilterMap = {
|
|
@@ -244,14 +250,14 @@ function MaterialComponentView({ properties, loadedTextures }) {
|
|
|
244
250
|
return new Vector2((_a = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _a !== void 0 ? _a : 1, (_b = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _b !== void 0 ? _b : 1);
|
|
245
251
|
}, [finalNormalMap, normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0], normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]]);
|
|
246
252
|
if (!properties) {
|
|
247
|
-
return _jsx("
|
|
253
|
+
return _jsx("meshStandardNodeMaterial", { color: "red", wireframe: true });
|
|
248
254
|
}
|
|
249
|
-
const materialKey = (_c = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _c !== void 0 ? _c : 'no-texture'
|
|
255
|
+
const materialKey = `${(_c = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _c !== void 0 ? _c : 'no-texture'}:${materialProps.transparent ? 'transparent' : 'opaque'}`;
|
|
250
256
|
const sharedProps = Object.assign({ map: finalTexture, side: resolvedSide }, materialProps);
|
|
251
257
|
if (materialType === 'basic') {
|
|
252
|
-
return _jsx("
|
|
258
|
+
return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps), materialKey);
|
|
253
259
|
}
|
|
254
|
-
return (_jsx("
|
|
260
|
+
return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
|
|
255
261
|
}
|
|
256
262
|
const MaterialComponent = {
|
|
257
263
|
name: 'Material',
|