react-three-game 0.0.36 → 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 (34) 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/InstanceProvider.d.ts +1 -0
  12. package/dist/tools/prefabeditor/InstanceProvider.js +51 -7
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +10 -2
  14. package/dist/tools/prefabeditor/PrefabEditor.js +60 -53
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +7 -2
  16. package/dist/tools/prefabeditor/PrefabRoot.js +78 -49
  17. package/dist/tools/prefabeditor/components/Input.d.ts +2 -1
  18. package/dist/tools/prefabeditor/components/Input.js +9 -3
  19. package/dist/tools/prefabeditor/components/PhysicsComponent.js +8 -7
  20. package/dist/tools/prefabeditor/components/TransformComponent.js +11 -3
  21. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  22. package/dist/tools/prefabeditor/utils.js +41 -0
  23. package/package.json +1 -1
  24. package/src/index.ts +12 -12
  25. package/src/tools/prefabeditor/EditorContext.tsx +20 -0
  26. package/src/tools/prefabeditor/EditorTree.tsx +83 -22
  27. package/src/tools/prefabeditor/EditorUI.tsx +2 -10
  28. package/src/tools/prefabeditor/InstanceProvider.tsx +60 -4
  29. package/src/tools/prefabeditor/PrefabEditor.tsx +80 -51
  30. package/src/tools/prefabeditor/PrefabRoot.tsx +181 -69
  31. package/src/tools/prefabeditor/components/Input.tsx +11 -3
  32. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +20 -7
  33. package/src/tools/prefabeditor/components/TransformComponent.tsx +25 -4
  34. 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
+ }
@@ -19,6 +19,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, regis
19
19
  selectedId?: string | null;
20
20
  editMode?: boolean;
21
21
  }): import("react/jsx-runtime").JSX.Element;
22
+ export declare function useInstanceCheck(id: string): boolean;
22
23
  export declare const GameInstance: React.ForwardRefExoticComponent<{
23
24
  id: string;
24
25
  modelUrl: string;
@@ -50,6 +50,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
50
50
  return prev.filter(i => i.id !== id);
51
51
  });
52
52
  }, []);
53
+ const hasInstance = useCallback((id) => {
54
+ return instances.some(i => i.id === id);
55
+ }, [instances]);
53
56
  // Flatten all model meshes once (models → flat mesh parts)
54
57
  // Note: Geometry is cloned with baked transforms for instancing
55
58
  const { flatMeshes, modelParts } = useMemo(() => {
@@ -97,7 +100,8 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
97
100
  removeInstance,
98
101
  instances,
99
102
  meshes: flatMeshes,
100
- modelParts
103
+ modelParts,
104
+ hasInstance
101
105
  }, children: [children, Object.entries(grouped).map(([key, group]) => {
102
106
  if (group.physicsType === 'none')
103
107
  return null;
@@ -105,7 +109,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
105
109
  const partCount = modelParts[modelKey] || 0;
106
110
  if (partCount === 0)
107
111
  return null;
108
- return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes }, key));
112
+ return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
109
113
  }), Object.entries(grouped).map(([key, group]) => {
110
114
  if (group.physicsType !== 'none')
111
115
  return null;
@@ -123,8 +127,9 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
123
127
  })] }));
124
128
  }
125
129
  // Render physics-enabled instances using InstancedRigidBodies
126
- function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
130
+ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
127
131
  const meshRefs = useRef([]);
132
+ const rigidBodiesRef = useRef(null);
128
133
  const instances = useMemo(() => group.instances.map(inst => ({
129
134
  key: inst.id,
130
135
  position: inst.position,
@@ -151,14 +156,47 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes }) {
151
156
  });
152
157
  mesh.instanceMatrix.needsUpdate = true;
153
158
  });
159
+ // Update rigid body positions when instances change
160
+ if (rigidBodiesRef.current) {
161
+ try {
162
+ group.instances.forEach((inst, i) => {
163
+ var _a;
164
+ const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a.at(i);
165
+ if (body && body.setTranslation && body.setRotation) {
166
+ pos.set(...inst.position);
167
+ euler.set(...inst.rotation);
168
+ quat.setFromEuler(euler);
169
+ body.setTranslation(pos, false);
170
+ body.setRotation(quat, false);
171
+ }
172
+ });
173
+ }
174
+ catch (error) {
175
+ // Ignore errors when switching between instanced/non-instanced states
176
+ console.warn('Failed to update rigidbody positions:', error);
177
+ }
178
+ }
154
179
  }, [group.instances]);
155
180
  const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
156
- return (_jsx(InstancedRigidBodies, { instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
181
+ // Handle click on instanced mesh in edit mode
182
+ const handleClick = (e) => {
183
+ if (!editMode || !onSelect)
184
+ return;
185
+ e.stopPropagation();
186
+ // Get the instance index from the intersection
187
+ const instanceId = e.instanceId;
188
+ if (instanceId !== undefined && group.instances[instanceId]) {
189
+ onSelect(group.instances[instanceId].id);
190
+ }
191
+ };
192
+ // Add key to force remount when instance count changes significantly (helps with cleanup)
193
+ const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
194
+ return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
157
195
  const mesh = flatMeshes[`${modelKey}__${i}`];
158
196
  if (!mesh)
159
197
  return null;
160
- return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false }, i));
161
- }) }));
198
+ return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false, onClick: editMode ? handleClick : undefined }, i));
199
+ }) }, rigidBodyKey));
162
200
  }
163
201
  // Render non-physics instances using Merged (instancing without rigid bodies)
164
202
  function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef, selectedId, editMode }) {
@@ -184,6 +222,12 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
184
222
  clickValid.current = false;
185
223
  }, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
186
224
  }
225
+ // Hook to check if an instance exists
226
+ export function useInstanceCheck(id) {
227
+ var _a;
228
+ const ctx = useContext(GameInstanceContext);
229
+ return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
230
+ }
187
231
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
188
232
  export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
189
233
  const ctx = useContext(GameInstanceContext);
@@ -196,7 +240,7 @@ export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation
196
240
  rotation,
197
241
  scale,
198
242
  physics,
199
- }), [id, modelUrl, position, rotation, scale, physics]);
243
+ }), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
200
244
  useEffect(() => {
201
245
  if (!addInstance || !removeInstance)
202
246
  return;
@@ -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, { 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,19 +1,24 @@
1
1
  import { Group, Matrix4, Object3D, Texture } from "three";
2
+ import { ThreeEvent } from "@react-three/fiber";
2
3
  import { Prefab, GameObject as GameObjectType } from "./types";
4
+ export interface PrefabRootRef {
5
+ root: Group | null;
6
+ }
3
7
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
4
8
  editMode?: boolean;
5
9
  data: Prefab;
6
10
  onPrefabChange?: (data: Prefab) => void;
7
11
  selectedId?: string | null;
8
12
  onSelect?: (id: string | null) => void;
9
- transformMode?: "translate" | "rotate" | "scale";
13
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
10
14
  basePath?: string;
11
- } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
15
+ } & import("react").RefAttributes<PrefabRootRef>>;
12
16
  export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
13
17
  interface RendererProps {
14
18
  gameObject: GameObjectType;
15
19
  selectedId?: string | null;
16
20
  onSelect?: (id: string) => void;
21
+ onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
17
22
  registerRef: (id: string, obj: Object3D | null) => void;
18
23
  loadedModels: Record<string, Object3D>;
19
24
  loadedTextures: Record<string, Texture>;