react-three-game 0.0.37 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.js +5 -5
  3. package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
  4. package/dist/tools/prefabeditor/EditorContext.js +9 -0
  5. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -3
  6. package/dist/tools/prefabeditor/EditorTree.js +38 -3
  7. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -5
  8. package/dist/tools/prefabeditor/EditorUI.js +4 -2
  9. package/dist/tools/prefabeditor/ExportHelper.d.ts +7 -0
  10. package/dist/tools/prefabeditor/ExportHelper.js +55 -0
  11. package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
  12. package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
  13. package/dist/tools/prefabeditor/PrefabRoot.d.ts +4 -2
  14. package/dist/tools/prefabeditor/PrefabRoot.js +18 -41
  15. package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
  16. package/dist/tools/prefabeditor/components/Input.js +9 -3
  17. package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
  18. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  19. package/dist/tools/prefabeditor/utils.js +41 -0
  20. package/package.json +1 -1
  21. package/src/index.ts +12 -12
  22. package/src/tools/prefabeditor/EditorContext.tsx +20 -0
  23. package/src/tools/prefabeditor/EditorTree.tsx +83 -22
  24. package/src/tools/prefabeditor/EditorUI.tsx +2 -10
  25. package/src/tools/prefabeditor/PrefabEditor.tsx +79 -50
  26. package/src/tools/prefabeditor/PrefabRoot.tsx +26 -64
  27. package/src/tools/prefabeditor/components/Input.tsx +11 -3
  28. package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
  29. package/src/tools/prefabeditor/utils.ts +43 -1
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export { default as GameCanvas } from './shared/GameCanvas';
2
2
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
3
4
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
- export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
5
- export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
5
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
6
6
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
7
  export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
8
+ export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
8
9
  export * as editorStyles from './tools/prefabeditor/styles';
9
10
  export * from './tools/prefabeditor/utils';
11
+ export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
12
+ export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
10
13
  export * from './helpers';
11
- export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- // Components
1
+ // Core Components
2
2
  export { default as GameCanvas } from './shared/GameCanvas';
3
+ // Prefab Editor
3
4
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
4
5
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
5
- export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
6
- export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
7
- // Component Registry
8
6
  export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
9
- // Editor Styles & Utils
10
7
  export * as editorStyles from './tools/prefabeditor/styles';
11
8
  export * from './tools/prefabeditor/utils';
9
+ // Asset Tools
10
+ export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
11
+ export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
12
12
  // Helpers
13
13
  export * from './helpers';
@@ -0,0 +1,11 @@
1
+ interface EditorContextType {
2
+ transformMode: "translate" | "rotate" | "scale";
3
+ setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
4
+ snapResolution: number;
5
+ setSnapResolution: (resolution: number) => void;
6
+ onScreenshot?: () => void;
7
+ onExportGLB?: () => void;
8
+ }
9
+ export declare const EditorContext: import("react").Context<EditorContextType | null>;
10
+ export declare function useEditorContext(): EditorContextType;
11
+ export {};
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from "react";
2
+ export const EditorContext = createContext(null);
3
+ export function useEditorContext() {
4
+ const context = useContext(EditorContext);
5
+ if (!context) {
6
+ throw new Error("useEditorContext must be used within EditorContext.Provider");
7
+ }
8
+ return context;
9
+ }
@@ -1,12 +1,10 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }: {
3
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
7
7
  setSelectedId: Dispatch<SetStateAction<string | null>>;
8
- onSave?: () => void;
9
- onLoad?: () => void;
10
8
  onUndo?: () => void;
11
9
  onRedo?: () => void;
12
10
  canUndo?: boolean;
@@ -1,9 +1,19 @@
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
+ };
1
10
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
11
  import { useState } from 'react';
3
12
  import { getComponent } from './components/ComponentRegistry';
4
13
  import { base, tree, menu } from './styles';
5
- import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
6
- export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }) {
14
+ import { findNode, findParent, deleteNode, cloneNode, updateNodeById, loadJson, saveJson, regenerateIds } from './utils';
15
+ import { useEditorContext } from './EditorContext';
16
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId, onUndo, onRedo, canUndo, canRedo }) {
7
17
  const [contextMenu, setContextMenu] = useState(null);
8
18
  const [draggedId, setDraggedId] = useState(null);
9
19
  const [collapsedIds, setCollapsedIds] = useState(new Set());
@@ -111,5 +121,30 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
111
121
  visibility: hasChildren ? 'visible' : 'hidden'
112
122
  }, 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 })] }), !isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))] }, node.id));
113
123
  };
114
- return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, 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" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: 28, right: 0 }), onClick: (e) => e.stopPropagation(), children: [_jsx("button", { style: menu.item, onClick: () => { onLoad === null || onLoad === void 0 ? void 0 : onLoad(); setFileMenuOpen(false); }, children: "\uD83D\uDCE5 Load" }), _jsx("button", { style: menu.item, onClick: () => { onSave === null || onSave === void 0 ? void 0 : onSave(); setFileMenuOpen(false); }, children: "\uD83D\uDCBE Save" })] }))] })] }))] }), !collapsed && _jsx("div", { style: tree.scroll, children: renderNode(prefabData.root) })] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
124
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: Object.assign(Object.assign({}, tree.panel), { width: collapsed ? 'auto' : 224 }), onClick: () => { setContextMenu(null); setFileMenuOpen(false); }, 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" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => { e.stopPropagation(); setFileMenuOpen(!fileMenuOpen); }, title: "File", children: "\u22EE" }), fileMenuOpen && (_jsx(FileMenu, { prefabData: prefabData, setPrefabData: setPrefabData, onClose: () => setFileMenuOpen(false) }))] })] }))] }), !collapsed && _jsx("div", { style: tree.scroll, children: renderNode(prefabData.root) })] }), contextMenu && (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: contextMenu.y, left: contextMenu.x }), onClick: (e) => e.stopPropagation(), onPointerLeave: () => setContextMenu(null), children: [_jsx("button", { style: menu.item, onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { style: menu.item, onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { style: Object.assign(Object.assign({}, menu.item), menu.danger), onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
125
+ }
126
+ function FileMenu({ prefabData, setPrefabData, onClose }) {
127
+ const { onScreenshot, onExportGLB } = useEditorContext();
128
+ const handleLoad = () => __awaiter(this, void 0, void 0, function* () {
129
+ const loadedPrefab = yield loadJson();
130
+ if (!loadedPrefab)
131
+ return;
132
+ setPrefabData(loadedPrefab);
133
+ onClose();
134
+ });
135
+ const handleSave = () => {
136
+ saveJson(prefabData, "prefab");
137
+ onClose();
138
+ };
139
+ const handleLoadIntoScene = () => __awaiter(this, void 0, void 0, function* () {
140
+ const loadedPrefab = yield loadJson();
141
+ if (!loadedPrefab)
142
+ return;
143
+ setPrefabData(prev => (Object.assign(Object.assign({}, prev), { root: updateNodeById(prev.root, prev.root.id, root => {
144
+ var _a;
145
+ return (Object.assign(Object.assign({}, root), { children: [...((_a = root.children) !== null && _a !== void 0 ? _a : []), regenerateIds(loadedPrefab.root)] }));
146
+ }) })));
147
+ onClose();
148
+ });
149
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, menu.container), { top: 28, right: 0 }), 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" })] }));
115
150
  }
@@ -1,15 +1,11 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }: {
3
+ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
7
7
  setSelectedId: Dispatch<SetStateAction<string | null>>;
8
- transformMode: "translate" | "rotate" | "scale";
9
- setTransformMode: (m: "translate" | "rotate" | "scale") => void;
10
8
  basePath?: string;
11
- onSave?: () => void;
12
- onLoad?: () => void;
13
9
  onUndo?: () => void;
14
10
  onRedo?: () => void;
15
11
  canUndo?: boolean;
@@ -15,8 +15,10 @@ import EditorTree from './EditorTree';
15
15
  import { getAllComponents } from './components/ComponentRegistry';
16
16
  import { base, inspector } from './styles';
17
17
  import { findNode, updateNode, deleteNode } from './utils';
18
- function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath, onSave, onLoad, onUndo, onRedo, canUndo, canRedo }) {
18
+ import { useEditorContext } from './EditorContext';
19
+ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, basePath, onUndo, onRedo, canUndo, canRedo }) {
19
20
  const [collapsed, setCollapsed] = useState(false);
21
+ const { transformMode, setTransformMode } = useEditorContext();
20
22
  const updateNodeHandler = (updater) => {
21
23
  if (!prefabData || !setPrefabData || !selectedId)
22
24
  return;
@@ -34,7 +36,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
34
36
  .prefab-scroll::-webkit-scrollbar-track { background: transparent; }
35
37
  .prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
36
38
  .prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
37
- ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onSave: onSave, onLoad: onLoad, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
39
+ ` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId, onUndo: onUndo, onRedo: onRedo, canUndo: canUndo, canRedo: canRedo }) })] });
38
40
  }
39
41
  function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
40
42
  var _a;
@@ -0,0 +1,7 @@
1
+ export declare const SCREENSHOT_EVENT = "prefab-editor-screenshot";
2
+ export declare const EXPORT_GLB_EVENT = "prefab-editor-export-glb";
3
+ export declare function ExportHelper({ prefabName }: {
4
+ prefabName: string;
5
+ }): null;
6
+ export declare function triggerScreenshot(): void;
7
+ export declare function triggerExportGLB(): void;
@@ -0,0 +1,55 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useThree } from "@react-three/fiber";
3
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
4
+ // Custom events for triggering exports
5
+ export const SCREENSHOT_EVENT = "prefab-editor-screenshot";
6
+ export const EXPORT_GLB_EVENT = "prefab-editor-export-glb";
7
+ export function ExportHelper({ prefabName }) {
8
+ const { gl, scene } = useThree();
9
+ const sceneRef = useRef(scene);
10
+ useEffect(() => {
11
+ sceneRef.current = scene;
12
+ }, [scene]);
13
+ useEffect(() => {
14
+ const handleScreenshot = () => {
15
+ const canvas = gl.domElement;
16
+ canvas.toBlob((blob) => {
17
+ if (!blob)
18
+ return;
19
+ const url = URL.createObjectURL(blob);
20
+ const a = document.createElement('a');
21
+ a.href = url;
22
+ a.download = `${prefabName || 'screenshot'}.png`;
23
+ a.click();
24
+ URL.revokeObjectURL(url);
25
+ });
26
+ };
27
+ const handleExportGLB = () => {
28
+ const exporter = new GLTFExporter();
29
+ exporter.parse(sceneRef.current, (result) => {
30
+ const blob = new Blob([result], { type: 'application/octet-stream' });
31
+ const url = URL.createObjectURL(blob);
32
+ const a = document.createElement('a');
33
+ a.href = url;
34
+ a.download = `${prefabName || 'scene'}.glb`;
35
+ a.click();
36
+ URL.revokeObjectURL(url);
37
+ }, (error) => {
38
+ console.error('Error exporting GLB:', error);
39
+ }, { binary: true });
40
+ };
41
+ window.addEventListener(SCREENSHOT_EVENT, handleScreenshot);
42
+ window.addEventListener(EXPORT_GLB_EVENT, handleExportGLB);
43
+ return () => {
44
+ window.removeEventListener(SCREENSHOT_EVENT, handleScreenshot);
45
+ window.removeEventListener(EXPORT_GLB_EVENT, handleExportGLB);
46
+ };
47
+ }, [gl, prefabName]);
48
+ return null;
49
+ }
50
+ export function triggerScreenshot() {
51
+ window.dispatchEvent(new Event(SCREENSHOT_EVENT));
52
+ }
53
+ export function triggerExportGLB() {
54
+ window.dispatchEvent(new Event(EXPORT_GLB_EVENT));
55
+ }
@@ -1,8 +1,16 @@
1
1
  import { Prefab } from "./types";
2
- declare const PrefabEditor: ({ basePath, initialPrefab, onPrefabChange, children }: {
2
+ import { PrefabRootRef } from "./PrefabRoot";
3
+ export interface PrefabEditorRef {
4
+ screenshot: () => void;
5
+ exportGLB: () => void;
6
+ prefab: Prefab;
7
+ setPrefab: (prefab: Prefab) => void;
8
+ rootRef: React.RefObject<PrefabRootRef | null>;
9
+ }
10
+ declare const PrefabEditor: import("react").ForwardRefExoticComponent<{
3
11
  basePath?: string;
4
12
  initialPrefab?: Prefab;
5
13
  onPrefabChange?: (prefab: Prefab) => void;
6
14
  children?: React.ReactNode;
7
- }) => import("react/jsx-runtime").JSX.Element;
15
+ } & import("react").RefAttributes<PrefabEditorRef>>;
8
16
  export default PrefabEditor;
@@ -1,20 +1,13 @@
1
1
  "use client";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
3
  import GameCanvas from "../../shared/GameCanvas";
13
- import { useState, useRef, useEffect } from "react";
4
+ import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
14
5
  import PrefabRoot from "./PrefabRoot";
15
6
  import { Physics } from "@react-three/rapier";
16
7
  import EditorUI from "./EditorUI";
17
8
  import { base, toolbar } from "./styles";
9
+ import { EditorContext } from "./EditorContext";
10
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
18
11
  const DEFAULT_PREFAB = {
19
12
  id: "prefab-default",
20
13
  name: "New Prefab",
@@ -28,15 +21,18 @@ const DEFAULT_PREFAB = {
28
21
  }
29
22
  }
30
23
  };
31
- const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
24
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, children }, ref) => {
32
25
  const [editMode, setEditMode] = useState(true);
33
26
  const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB);
34
27
  const [selectedId, setSelectedId] = useState(null);
35
28
  const [transformMode, setTransformMode] = useState("translate");
29
+ const [snapResolution, setSnapResolution] = useState(0);
36
30
  const [history, setHistory] = useState([loadedPrefab]);
37
31
  const [historyIndex, setHistoryIndex] = useState(0);
38
32
  const throttleRef = useRef(null);
39
33
  const lastDataRef = useRef(JSON.stringify(loadedPrefab));
34
+ const prefabRootRef = useRef(null);
35
+ const canvasRef = useRef(null);
40
36
  useEffect(() => {
41
37
  if (initialPrefab)
42
38
  setLoadedPrefab(initialPrefab);
@@ -87,48 +83,59 @@ const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) =>
87
83
  return () => { if (throttleRef.current)
88
84
  clearTimeout(throttleRef.current); };
89
85
  }, [loadedPrefab]);
90
- const handleLoad = () => __awaiter(void 0, void 0, void 0, function* () {
91
- const prefab = yield loadJson();
92
- if (prefab) {
93
- setLoadedPrefab(prefab);
94
- onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(prefab);
95
- setHistory([prefab]);
96
- setHistoryIndex(0);
97
- lastDataRef.current = JSON.stringify(prefab);
98
- }
99
- });
100
- return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { debug: editMode, paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, basePath: basePath }), children] }) }), _jsx("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath, onSave: () => saveJson(loadedPrefab, "prefab"), onLoad: handleLoad, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
101
- };
102
- const saveJson = (data, filename) => {
103
- const a = document.createElement('a');
104
- a.href = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
105
- a.download = `${filename || 'prefab'}.json`;
106
- a.click();
107
- };
108
- const loadJson = () => new Promise(resolve => {
109
- const input = document.createElement('input');
110
- input.type = 'file';
111
- input.accept = '.json,application/json';
112
- input.onchange = e => {
86
+ const handleScreenshot = () => {
87
+ const canvas = canvasRef.current;
88
+ if (!canvas)
89
+ return;
90
+ canvas.toBlob((blob) => {
91
+ if (!blob)
92
+ return;
93
+ const url = URL.createObjectURL(blob);
94
+ const a = document.createElement('a');
95
+ a.href = url;
96
+ a.download = `${loadedPrefab.name || 'screenshot'}.png`;
97
+ a.click();
98
+ URL.revokeObjectURL(url);
99
+ });
100
+ };
101
+ const handleExportGLB = () => {
113
102
  var _a;
114
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
115
- if (!file)
116
- return resolve(undefined);
117
- const reader = new FileReader();
118
- reader.onload = e => {
119
- var _a;
120
- try {
121
- const text = (_a = e.target) === null || _a === void 0 ? void 0 : _a.result;
122
- if (typeof text === 'string')
123
- resolve(JSON.parse(text));
124
- }
125
- catch (err) {
126
- console.error('Error parsing prefab JSON:', err);
127
- resolve(undefined);
128
- }
129
- };
130
- reader.readAsText(file);
103
+ const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
104
+ if (!sceneRoot)
105
+ return;
106
+ const exporter = new GLTFExporter();
107
+ exporter.parse(sceneRoot, (result) => {
108
+ const blob = new Blob([result], { type: 'application/octet-stream' });
109
+ const url = URL.createObjectURL(blob);
110
+ const a = document.createElement('a');
111
+ a.href = url;
112
+ a.download = `${loadedPrefab.name || 'scene'}.glb`;
113
+ a.click();
114
+ URL.revokeObjectURL(url);
115
+ }, (error) => {
116
+ console.error('Error exporting GLB:', error);
117
+ }, { binary: true });
131
118
  };
132
- input.click();
119
+ useEffect(() => {
120
+ const canvas = document.querySelector('canvas');
121
+ if (canvas)
122
+ canvasRef.current = canvas;
123
+ }, []);
124
+ useImperativeHandle(ref, () => ({
125
+ screenshot: handleScreenshot,
126
+ exportGLB: handleExportGLB,
127
+ prefab: loadedPrefab,
128
+ setPrefab: setLoadedPrefab,
129
+ rootRef: prefabRootRef
130
+ }), [loadedPrefab]);
131
+ return _jsxs(EditorContext.Provider, { value: {
132
+ transformMode,
133
+ setTransformMode,
134
+ snapResolution,
135
+ setSnapResolution,
136
+ onScreenshot: handleScreenshot,
137
+ onExportGLB: handleExportGLB
138
+ }, children: [_jsx(GameCanvas, { children: _jsxs(Physics, { debug: editMode, paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, basePath: basePath }), children] }) }), _jsx("div", { style: toolbar.panel, children: _jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }) }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] });
133
139
  });
140
+ PrefabEditor.displayName = "PrefabEditor";
134
141
  export default PrefabEditor;
@@ -1,6 +1,9 @@
1
1
  import { Group, Matrix4, Object3D, Texture } from "three";
2
2
  import { ThreeEvent } from "@react-three/fiber";
3
3
  import { Prefab, GameObject as GameObjectType } from "./types";
4
+ export interface PrefabRootRef {
5
+ root: Group | null;
6
+ }
4
7
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
5
8
  editMode?: boolean;
6
9
  data: Prefab;
@@ -8,9 +11,8 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
8
11
  selectedId?: string | null;
9
12
  onSelect?: (id: string | null) => void;
10
13
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
11
- transformMode?: "translate" | "rotate" | "scale";
12
14
  basePath?: string;
13
- } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
15
+ } & import("react").RefAttributes<PrefabRootRef>>;
14
16
  export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
15
17
  interface RendererProps {
16
18
  gameObject: GameObjectType;
@@ -10,50 +10,50 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  import { MapControls, TransformControls, useHelper } from "@react-three/drei";
13
- import { forwardRef, useCallback, useEffect, useRef, useState, } from "react";
13
+ import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
14
14
  import { BoxHelper, Euler, Matrix4, Quaternion, SRGBColorSpace, TextureLoader, Vector3, } from "three";
15
15
  import { getComponent, registerComponent } from "./components/ComponentRegistry";
16
16
  import components from "./components";
17
17
  import { loadModel } from "../dragdrop/modelLoader";
18
18
  import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
19
19
  import { updateNode } from "./utils";
20
- /* -------------------------------------------------- */
21
- /* Setup */
22
- /* -------------------------------------------------- */
20
+ import { EditorContext } from "./EditorContext";
23
21
  components.forEach(registerComponent);
24
22
  const IDENTITY = new Matrix4();
25
- /* -------------------------------------------------- */
26
- /* PrefabRoot */
27
- /* -------------------------------------------------- */
28
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, transformMode, basePath = "" }, ref) => {
23
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, onClick, basePath = "" }, ref) => {
24
+ var _a, _b;
25
+ // optional editor context
26
+ const editorContext = useContext(EditorContext);
27
+ const transformMode = (_a = editorContext === null || editorContext === void 0 ? void 0 : editorContext.transformMode) !== null && _a !== void 0 ? _a : "translate";
28
+ const snapResolution = (_b = editorContext === null || editorContext === void 0 ? void 0 : editorContext.snapResolution) !== null && _b !== void 0 ? _b : 0;
29
+ // prefab root state
29
30
  const [models, setModels] = useState({});
30
31
  const [textures, setTextures] = useState({});
31
32
  const loading = useRef(new Set());
32
33
  const objectRefs = useRef({});
33
34
  const [selectedObject, setSelectedObject] = useState(null);
35
+ const rootRef = useRef(null);
36
+ useImperativeHandle(ref, () => ({
37
+ root: rootRef.current
38
+ }), []);
34
39
  const registerRef = useCallback((id, obj) => {
35
40
  objectRefs.current[id] = obj;
36
41
  if (id === selectedId)
37
42
  setSelectedObject(obj);
38
43
  }, [selectedId]);
39
- // Suppress TransformControls scene graph warnings during transitions
40
44
  useEffect(() => {
41
45
  const originalError = console.error;
42
46
  console.error = (...args) => {
43
- if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph')) {
44
- return; // Suppress this specific error
45
- }
47
+ if (typeof args[0] === 'string' && args[0].includes('TransformControls') && args[0].includes('scene graph'))
48
+ return;
46
49
  originalError.apply(console, args);
47
50
  };
48
- return () => {
49
- console.error = originalError;
50
- };
51
+ return () => { console.error = originalError; };
51
52
  }, []);
52
53
  useEffect(() => {
53
54
  var _a;
54
55
  setSelectedObject(selectedId ? (_a = objectRefs.current[selectedId]) !== null && _a !== void 0 ? _a : null : null);
55
56
  }, [selectedId]);
56
- /* ---------------- Transform writeback ---------------- */
57
57
  const onTransformChange = () => {
58
58
  if (!selectedId || !onPrefabChange)
59
59
  return;
@@ -69,7 +69,6 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
69
69
  } }) })));
70
70
  onPrefabChange(Object.assign(Object.assign({}, data), { root }));
71
71
  };
72
- /* ---------------- Asset loading ---------------- */
73
72
  useEffect(() => {
74
73
  const modelsToLoad = new Set();
75
74
  const texturesToLoad = new Set();
@@ -105,12 +104,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
105
104
  });
106
105
  });
107
106
  }, [data, models, textures]);
108
- /* ---------------- Render ---------------- */
109
- return (_jsxs("group", { ref: ref, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange }))] }))] }));
107
+ return (_jsxs("group", { ref: rootRef, children: [_jsx(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(GameObjectRenderer, { gameObject: data.root, selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: models, loadedTextures: textures, editMode: editMode, parentMatrix: IDENTITY }) }), editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: onTransformChange, translationSnap: snapResolution > 0 ? snapResolution : undefined, rotationSnap: snapResolution > 0 ? snapResolution : undefined, scaleSnap: snapResolution > 0 ? snapResolution : undefined }, `transform-${snapResolution}`))] }))] }));
110
108
  });
111
- /* -------------------------------------------------- */
112
- /* Renderer Switch */
113
- /* -------------------------------------------------- */
114
109
  export function GameObjectRenderer(props) {
115
110
  var _a, _b, _c;
116
111
  const node = props.gameObject;
@@ -120,16 +115,13 @@ export function GameObjectRenderer(props) {
120
115
  const prevInstancedRef = useRef(undefined);
121
116
  const [isTransitioning, setIsTransitioning] = useState(false);
122
117
  useEffect(() => {
123
- // Detect instanced mode change
124
118
  if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
125
119
  setIsTransitioning(true);
126
- // Wait for cleanup, then allow new mode to render
127
120
  const timer = setTimeout(() => setIsTransitioning(false), 100);
128
121
  return () => clearTimeout(timer);
129
122
  }
130
123
  prevInstancedRef.current = isInstanced;
131
124
  }, [isInstanced]);
132
- // Don't render during transition to avoid physics conflicts
133
125
  if (isTransitioning)
134
126
  return null;
135
127
  const key = `${node.id}_${isInstanced ? 'instanced' : 'standard'}`;
@@ -137,9 +129,6 @@ export function GameObjectRenderer(props) {
137
129
  ? _jsx(InstancedNode, Object.assign({}, props), key)
138
130
  : _jsx(StandardNode, Object.assign({}, props), key);
139
131
  }
140
- /* -------------------------------------------------- */
141
- /* InstancedNode (terminal) */
142
- /* -------------------------------------------------- */
143
132
  function isPhysicsProps(v) {
144
133
  return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic";
145
134
  }
@@ -147,7 +136,6 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
147
136
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
148
137
  const world = parentMatrix.clone().multiply(compose(gameObject));
149
138
  const { position: worldPosition, rotation: worldRotation, scale: worldScale } = decompose(world);
150
- // Get local transform for proxy group (used by transform controls)
151
139
  const localTransform = getNodeTransformProps(gameObject);
152
140
  const physicsProps = isPhysicsProps((_b = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics) === null || _b === void 0 ? void 0 : _b.properties)
153
141
  ? (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.physics) === null || _d === void 0 ? void 0 : _d.properties
@@ -161,8 +149,6 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
161
149
  }
162
150
  }, [gameObject.id, registerRef, editMode]);
163
151
  const modelUrl = (_g = (_f = (_e = gameObject.components) === null || _e === void 0 ? void 0 : _e.model) === null || _f === void 0 ? void 0 : _f.properties) === null || _g === void 0 ? void 0 : _g.filename;
164
- // In edit mode, create a proxy group at the same position for transform controls
165
- // The GameInstance still needs the actual position so it renders correctly
166
152
  if (editMode) {
167
153
  return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
168
154
  if (clickValid.current) {
@@ -175,18 +161,13 @@ function InstancedNode({ gameObject, parentMatrix = IDENTITY, editMode, register
175
161
  }
176
162
  return (_jsx(GameInstance, { id: gameObject.id, modelUrl: (_k = (_j = (_h = gameObject.components) === null || _h === void 0 ? void 0 : _h.model) === null || _j === void 0 ? void 0 : _j.properties) === null || _k === void 0 ? void 0 : _k.filename, position: worldPosition, rotation: worldRotation, scale: worldScale, physics: physicsProps }));
177
163
  }
178
- /* -------------------------------------------------- */
179
- /* StandardNode */
180
- /* -------------------------------------------------- */
181
164
  function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
182
165
  var _a, _b, _c, _d, _e, _f;
183
166
  const groupRef = useRef(null);
184
167
  const helperRef = useRef(null);
185
168
  const clickValid = useRef(false);
186
169
  const isSelected = selectedId === gameObject.id;
187
- // Check if this object still exists as an instance (to prevent physics overlap)
188
170
  const stillInstanced = useInstanceCheck(gameObject.id);
189
- // Use helperRef for BoxHelper (shows actual content bounds at correct position)
190
171
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
191
172
  useEffect(() => {
192
173
  registerRef(gameObject.id, groupRef.current);
@@ -210,16 +191,13 @@ function StandardNode({ gameObject, selectedId, onSelect, onClick, registerRef,
210
191
  loadedModels[gameObject.components.model.properties.filename];
211
192
  const hasPhysics = physics && ready && !stillInstanced;
212
193
  const transform = getNodeTransformProps(gameObject);
213
- // Prepare physics wrapper if needed
214
194
  const physicsDef = hasPhysics ? getComponent("Physics") : null;
215
195
  const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
216
196
  const physicsKey = `physics_${gameObject.id}_${isInstanced ? 'instanced' : 'standard'}`;
217
- const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { child, gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
218
- // In edit mode, use proxy group pattern
197
+ const inner = (_jsxs("group", { onPointerDown: editMode ? onDown : undefined, onPointerMove: editMode ? () => (clickValid.current = false) : undefined, onPointerUp: editMode ? onUp : undefined, children: [renderCoreNode(gameObject, { loadedModels, loadedTextures, editMode, registerRef }, parentMatrix), (_f = gameObject.children) === null || _f === void 0 ? void 0 : _f.map(child => (_jsx(GameObjectRenderer, { gameObject: child, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode, parentMatrix: world }, child.id)))] }));
219
198
  if (editMode) {
220
199
  return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey)) : null] }));
221
200
  }
222
- // In play mode, apply transform directly to content
223
201
  if (hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View)) {
224
202
  return (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, children: inner }, physicsKey));
225
203
  }
@@ -293,7 +271,6 @@ function renderCoreNode(gameObject, ctx, parentMatrix) {
293
271
  const def = getComponent(comp.type);
294
272
  if (!(def === null || def === void 0 ? void 0 : def.View))
295
273
  return;
296
- // crude but works with your existing component API
297
274
  if (def.View.toString().includes("children")) {
298
275
  wrappers.push({ key, View: def.View, properties: comp.properties });
299
276
  }