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 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
- **Wrapper** components accept `children` (animations, controllers). **Leaf** components don't (lights, particles).
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. Import/export JSON. Physics only runs in play mode.
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` `<Merged>` + `<InstancedRigidBodies>`
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, menu } from './styles';
14
- import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
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); }, 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: {
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("button", { ref: ref, style: {
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
- }, onClick: (e) => {
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("button", { ref: ref, style: {
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
- }, onClick: (e) => {
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 (_jsx(_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("button", { ref: ref, style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); toggle(); }, title: "File", 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) })] }))] }) }));
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, useHelper } from '@react-three/drei';
3
- import { useRef } from 'react';
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
- return (_jsxs(FieldGroup, { children: [_jsx(NumberField, { name: "fov", label: "FOV", values: component.properties, onChange: onUpdate, fallback: 50, min: 1, max: 179, step: 1 }), _jsx(NumberField, { name: "near", label: "Near", values: component.properties, onChange: onUpdate, fallback: 0.1, min: 0.001, step: 0.1 }), _jsx(NumberField, { name: "zoom", label: "Zoom", values: component.properties, onChange: onUpdate, fallback: 1, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "far", label: "Far", values: component.properties, onChange: onUpdate, fallback: 1000, min: 0.1, step: 1 })] }));
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
- var _a, _b, _c, _d;
11
- const fov = (_a = properties === null || properties === void 0 ? void 0 : properties.fov) !== null && _a !== void 0 ? _a : 50;
12
- const near = (_b = properties === null || properties === void 0 ? void 0 : properties.near) !== null && _b !== void 0 ? _b : 0.1;
13
- const zoom = (_c = properties === null || properties === void 0 ? void 0 : properties.zoom) !== null && _c !== void 0 ? _c : 1;
14
- const far = (_d = properties === null || properties === void 0 ? void 0 : properties.far) !== null && _d !== void 0 ? _d : 1000;
15
- const cameraRef = useRef(null);
16
- useHelper(editMode && isSelected ? cameraRef : null, CameraHelper);
17
- return (_jsxs(_Fragment, { children: [_jsx(PerspectiveCamera, { ref: cameraRef, makeDefault: !editMode, fov: fov, near: near, zoom: zoom, far: far }), editMode && !isSelected ? (_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [0.34, 0.22, 0.18] }), _jsx("meshBasicMaterial", { color: "#22d3ee", wireframe: true })] })) : null] }));
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
- return (_jsx(FieldRenderer, { fields: directionalLightFields, values: component.properties, onChange: onUpdate }));
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
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
36
- const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
37
- const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
38
- const castShadow = (_c = properties.castShadow) !== null && _c !== void 0 ? _c : true;
39
- const shadowMapSize = (_d = properties.shadowMapSize) !== null && _d !== void 0 ? _d : 1024;
40
- const shadowCameraNear = (_e = properties.shadowCameraNear) !== null && _e !== void 0 ? _e : 0.1;
41
- const shadowCameraFar = (_f = properties.shadowCameraFar) !== null && _f !== void 0 ? _f : 100;
42
- const shadowCameraTop = (_g = properties.shadowCameraTop) !== null && _g !== void 0 ? _g : 30;
43
- const shadowCameraBottom = (_h = properties.shadowCameraBottom) !== null && _h !== void 0 ? _h : -30;
44
- const shadowCameraLeft = (_j = properties.shadowCameraLeft) !== null && _j !== void 0 ? _j : -30;
45
- const shadowCameraRight = (_k = properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30;
46
- const targetOffset = (_l = properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0];
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
- // Set up light target reference when both refs are ready
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
- const lightWorldPos = new Vector3();
60
- directionalLightRef.current.getWorldPosition(lightWorldPos);
61
- // Target is positioned relative to light's world position
62
- targetRef.current.position.set(lightWorldPos.x + targetOffset[0], lightWorldPos.y + targetOffset[1], lightWorldPos.z + targetOffset[2]);
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, metalness: _metalness, roughness: _roughness, transmission: _transmission, thickness: _thickness, ior: _ior } = materialSource, materialProps = __rest(materialSource, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side", "metalness", "roughness", "transmission", "thickness", "ior"]);
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("meshStandardMaterial", { color: "red", wireframe: true });
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("meshBasicMaterial", Object.assign({}, sharedProps), materialKey);
258
+ return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps), materialKey);
253
259
  }
254
- return (_jsx("meshStandardMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
260
+ return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
255
261
  }
256
262
  const MaterialComponent = {
257
263
  name: 'Material',