react-three-game 0.0.107 → 0.0.108

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 (28) hide show
  1. package/README.md +7 -0
  2. package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +74 -46
  3. package/dist/plugins/crashcat/CrashcatRagdoll.d.ts +58 -0
  4. package/dist/plugins/crashcat/CrashcatRagdoll.js +410 -0
  5. package/dist/plugins/crashcat/CrashcatRuntime.js +18 -14
  6. package/dist/plugins/crashcat/index.d.ts +1 -0
  7. package/dist/plugins/crashcat/index.js +1 -0
  8. package/dist/tools/assetviewer/page.js +4 -4
  9. package/dist/tools/prefabeditor/EditorTree.js +5 -2
  10. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +2 -1
  11. package/dist/tools/prefabeditor/EditorTreeMenus.js +17 -5
  12. package/dist/tools/prefabeditor/GameEvents.d.ts +1 -0
  13. package/dist/tools/prefabeditor/PrefabEditor.d.ts +2 -1
  14. package/dist/tools/prefabeditor/PrefabEditor.js +26 -13
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
  16. package/dist/tools/prefabeditor/PrefabRoot.js +61 -25
  17. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +15 -0
  18. package/dist/tools/prefabeditor/components/PrefabRefComponent.d.ts +3 -0
  19. package/dist/tools/prefabeditor/components/PrefabRefComponent.js +72 -0
  20. package/dist/tools/prefabeditor/components/TextComponent.js +8 -5
  21. package/dist/tools/prefabeditor/components/index.js +2 -0
  22. package/dist/tools/prefabeditor/modelPrefab.d.ts +3 -0
  23. package/dist/tools/prefabeditor/modelPrefab.js +44 -11
  24. package/dist/tools/prefabeditor/prefab.d.ts +5 -4
  25. package/dist/tools/prefabeditor/prefab.js +47 -29
  26. package/dist/tools/prefabeditor/utils.d.ts +8 -1
  27. package/dist/tools/prefabeditor/utils.js +74 -22
  28. package/package.json +13 -13
@@ -33,11 +33,14 @@ function setCrashcatApi(api) {
33
33
  listener();
34
34
  });
35
35
  }
36
- function emitConfiguredEvent(eventName, sourceNodeId, targetNodeId, collisionNormal) {
37
- const trimmed = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
38
- if (!trimmed)
36
+ function emitPhysicsEvent(eventType, eventName, sourceNodeId, targetNodeId, collisionNormal) {
37
+ const alias = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
38
+ if (!gameEvents.hasListeners(eventType) && (!alias || !gameEvents.hasListeners(alias)))
39
39
  return;
40
- gameEvents.emit(trimmed, Object.assign({ sourceEntityId: sourceNodeId, sourceNodeId, targetEntityId: targetNodeId, targetNodeId }, (collisionNormal ? { collisionNormal } : {})));
40
+ const payload = Object.assign({ sourceEntityId: sourceNodeId, sourceNodeId, targetEntityId: targetNodeId, targetNodeId }, (collisionNormal ? { collisionNormal } : {}));
41
+ gameEvents.emit(eventType, payload);
42
+ if (alias)
43
+ gameEvents.emit(alias, payload);
41
44
  }
42
45
  function createDebugState() {
43
46
  const options = debugRenderer.createDefaultOptions();
@@ -71,26 +74,27 @@ export function CrashcatRuntime({ debug = false, children }) {
71
74
  const [debugState] = useState(() => debug ? createDebugState() : null);
72
75
  const listener = useMemo(() => ({
73
76
  onContactAdded: (bodyA, bodyB, manifold) => {
74
- var _a, _b;
77
+ var _a, _b, _c, _d, _e, _f;
75
78
  const metaA = getBodyMeta(bodyByIdRef.current, bodyA);
76
79
  const metaB = getBodyMeta(bodyByIdRef.current, bodyB);
77
80
  const n = manifold === null || manifold === void 0 ? void 0 : manifold.worldSpaceNormal;
78
81
  const nA = n ? [n[0], n[1], n[2]] : undefined;
79
82
  const nB = n ? [-n[0], -n[1], -n[2]] : undefined;
80
- if (metaA === null || metaA === void 0 ? void 0 : metaA.events)
81
- emitConfiguredEvent(metaA.sensor ? metaA.events.sensorEnter : metaA.events.collisionEnter, metaA.nodeId, (_a = metaB === null || metaB === void 0 ? void 0 : metaB.nodeId) !== null && _a !== void 0 ? _a : null, nA);
82
- if (metaB === null || metaB === void 0 ? void 0 : metaB.events)
83
- emitConfiguredEvent(metaB.sensor ? metaB.events.sensorEnter : metaB.events.collisionEnter, metaB.nodeId, (_b = metaA === null || metaA === void 0 ? void 0 : metaA.nodeId) !== null && _b !== void 0 ? _b : null, nB);
83
+ if (metaA)
84
+ emitPhysicsEvent(metaA.sensor ? "sensor:enter" : "collision:enter", metaA.sensor ? (_a = metaA.events) === null || _a === void 0 ? void 0 : _a.sensorEnter : (_b = metaA.events) === null || _b === void 0 ? void 0 : _b.collisionEnter, metaA.nodeId, (_c = metaB === null || metaB === void 0 ? void 0 : metaB.nodeId) !== null && _c !== void 0 ? _c : null, nA);
85
+ if (metaB)
86
+ emitPhysicsEvent(metaB.sensor ? "sensor:enter" : "collision:enter", metaB.sensor ? (_d = metaB.events) === null || _d === void 0 ? void 0 : _d.sensorEnter : (_e = metaB.events) === null || _e === void 0 ? void 0 : _e.collisionEnter, metaB.nodeId, (_f = metaA === null || metaA === void 0 ? void 0 : metaA.nodeId) !== null && _f !== void 0 ? _f : null, nB);
84
87
  },
85
88
  onContactRemoved: (idA, idB) => {
86
- var _a, _b;
89
+ var _a, _b, _c, _d, _e, _f;
87
90
  const metaA = bodyByIdRef.current.get(Number(idA));
88
91
  const metaB = bodyByIdRef.current.get(Number(idB));
89
- if (metaA === null || metaA === void 0 ? void 0 : metaA.events)
90
- emitConfiguredEvent(metaA.sensor ? metaA.events.sensorExit : metaA.events.collisionExit, metaA.nodeId, (_a = metaB === null || metaB === void 0 ? void 0 : metaB.nodeId) !== null && _a !== void 0 ? _a : null);
91
- if (metaB === null || metaB === void 0 ? void 0 : metaB.events)
92
- emitConfiguredEvent(metaB.sensor ? metaB.events.sensorExit : metaB.events.collisionExit, metaB.nodeId, (_b = metaA === null || metaA === void 0 ? void 0 : metaA.nodeId) !== null && _b !== void 0 ? _b : null);
92
+ if (metaA)
93
+ emitPhysicsEvent(metaA.sensor ? "sensor:exit" : "collision:exit", metaA.sensor ? (_a = metaA.events) === null || _a === void 0 ? void 0 : _a.sensorExit : (_b = metaA.events) === null || _b === void 0 ? void 0 : _b.collisionExit, metaA.nodeId, (_c = metaB === null || metaB === void 0 ? void 0 : metaB.nodeId) !== null && _c !== void 0 ? _c : null);
94
+ if (metaB)
95
+ emitPhysicsEvent(metaB.sensor ? "sensor:exit" : "collision:exit", metaB.sensor ? (_d = metaB.events) === null || _d === void 0 ? void 0 : _d.sensorExit : (_e = metaB.events) === null || _e === void 0 ? void 0 : _e.collisionExit, metaB.nodeId, (_f = metaA === null || metaA === void 0 ? void 0 : metaA.nodeId) !== null && _f !== void 0 ? _f : null);
93
96
  },
97
+ onBodyPairValidate: (bodyA, bodyB) => !rigidBody.bodiesShareConstraint(bodyA, bodyB),
94
98
  }), []);
95
99
  useEffect(() => {
96
100
  ensureCrashcatRegistered();
@@ -1,2 +1,3 @@
1
1
  export { CrashcatRuntime, useCrashcat, type BodyMeta, type CrashcatApi, type CrashcatEventConfig, } from "./CrashcatRuntime";
2
2
  export { default as CrashcatPhysicsComponent, default, } from "./CrashcatPhysicsComponent";
3
+ export { CrashcatRagdoll, default as CrashcatRagdollComponent, RagdollBodyPart, createRagdollSettings, createStaticBoxBody, type CrashcatRagdollProps, type RagdollSettings, } from "./CrashcatRagdoll";
@@ -1,2 +1,3 @@
1
1
  export { CrashcatRuntime, useCrashcat, } from "./CrashcatRuntime";
2
2
  export { default as CrashcatPhysicsComponent, default, } from "./CrashcatPhysicsComponent";
3
+ export { CrashcatRagdoll, default as CrashcatRagdollComponent, RagdollBodyPart, createRagdollSettings, createStaticBoxBody, } from "./CrashcatRagdoll";
@@ -5,7 +5,7 @@ import { Suspense, useEffect, useLayoutEffect, useState, useRef } from "react";
5
5
  import { createPortal } from 'react-dom';
6
6
  import { Mesh, TextureLoader } from "three";
7
7
  import { loadModel } from "../dragdrop/modelLoader";
8
- import { resolvePrefabAssetPath } from "../prefabeditor/PrefabEditor";
8
+ import { withBasePath } from "../prefabeditor/utils";
9
9
  import { base, colors, fonts } from "../prefabeditor/styles";
10
10
  const styles = {
11
11
  errorIcon: { color: colors.danger, fontSize: 12 },
@@ -126,7 +126,7 @@ function TextureCard({ file, onSelect, basePath = "" }) {
126
126
  const [isHovered, setIsHovered] = useState(false);
127
127
  const [error, setError] = useState(false);
128
128
  const { ref, isInView } = useInView();
129
- const fullPath = resolvePrefabAssetPath(basePath, file);
129
+ const fullPath = withBasePath(basePath, file);
130
130
  const fileName = file.split('/').pop();
131
131
  if (error) {
132
132
  return (_jsxs("div", { ref: ref, style: Object.assign(Object.assign({ maxWidth: 60, aspectRatio: '1 / 1' }, assetTileStyle), { backgroundColor: assetViewerColors.errorBg }), onClick: () => onSelect(file), title: `Could not load ${file}`, children: [_jsx("div", { style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }), _jsx("div", { style: styles.bottomLabel, children: fileName })] }));
@@ -170,7 +170,7 @@ export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
170
170
  function ModelCard({ file, onSelect, basePath = "", size = 60, }) {
171
171
  const [error, setError] = useState(false);
172
172
  const { ref, isInView } = useInView();
173
- const fullPath = resolvePrefabAssetPath(basePath, file);
173
+ const fullPath = withBasePath(basePath, file);
174
174
  if (error) {
175
175
  return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: assetViewerColors.errorBg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', border: `1px solid ${colors.dangerBorder}` }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
176
176
  }
@@ -215,7 +215,7 @@ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
215
215
  }
216
216
  function SoundCard({ file, onSelect, basePath = "" }) {
217
217
  const fileName = file.split('/').pop() || '';
218
- const fullPath = resolvePrefabAssetPath(basePath, file);
218
+ const fullPath = withBasePath(basePath, file);
219
219
  return (_jsxs("div", { onClick: () => onSelect(file), style: Object.assign(Object.assign({ aspectRatio: '1 / 1' }, assetTileStyle), { alignItems: 'center', justifyContent: 'center' }), children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { color: colors.text, fontSize: fonts.size, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
220
220
  }
221
221
  const PICKER_POPUP_WIDTH = 260;
@@ -4,7 +4,7 @@ import { base, colors, tree } from './styles';
4
4
  import { useEditorContext, useEditorRef } from './PrefabEditor';
5
5
  import { Dropdown } from './Dropdown';
6
6
  import { FileMenu, TreeContextMenu, TreeNodeMenu } from './EditorTreeMenus';
7
- import { createEmptyNode } from './prefab';
7
+ import { createEmptyNode, createPackedPrefabNode } from './prefab';
8
8
  import { usePrefabChildIds, usePrefabNode, usePrefabRootId, usePrefabStore, usePrefabStoreApi } from './prefabStore';
9
9
  export default function EditorTree({ selectedId, setSelectedId, getPrefab, onReplacePrefab, onImportPrefab, onUndo, onRedo, canUndo, canRedo }) {
10
10
  const { onFocusNode } = useEditorContext();
@@ -30,6 +30,9 @@ export default function EditorTree({ selectedId, setSelectedId, getPrefab, onRep
30
30
  editor.add(newNode, parentId);
31
31
  setSelectedId(newNode.id);
32
32
  };
33
+ const handleImportPackedPrefab = (url) => {
34
+ editor.add(createPackedPrefabNode(url), rootId);
35
+ };
33
36
  const handleDuplicate = (nodeId) => {
34
37
  if (nodeId === rootId)
35
38
  return;
@@ -103,7 +106,7 @@ export default function EditorTree({ selectedId, setSelectedId, getPrefab, onRep
103
106
  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: "Prefab" })] }), !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, title: "Menu", style: Object.assign(Object.assign({}, base.btn), { padding: '2px 6px', fontSize: 10 }), onClick: (e) => {
104
107
  e.stopPropagation();
105
108
  toggle();
106
- }, children: "\u22EE" })), children: (close) => (_jsx(FileMenu, { getPrefab: getPrefab, onReplacePrefab: onReplacePrefab, onImportPrefab: onImportPrefab, 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", { style: tree.scroll, children: _jsx(TreeNode, { nodeId: rootId, depth: 0, rootId: rootId, visibleIds: visibleIds, collapsedIds: collapsedIds, dropTarget: dropTarget, selectedNodeId: selectedId, onToggleCollapse: toggleCollapse, onOpenContextMenu: openContextMenu, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onDragEnd: () => { setDraggedId(null); setDropTarget(null); }, renderTreeNodeMenu: renderTreeNodeMenu, onToggleDisabled: handleToggleDisabled, setSelectedId: setSelectedId }) })] }))] }), _jsx(TreeContextMenu, { contextMenu: contextMenu, onClose: closeContextMenu, children: (nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === rootId, close) })] }));
109
+ }, children: "\u22EE" })), children: (close) => (_jsx(FileMenu, { getPrefab: getPrefab, onReplacePrefab: onReplacePrefab, onImportPrefab: onImportPrefab, onImportPackedPrefab: handleImportPackedPrefab, 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", { style: tree.scroll, children: _jsx(TreeNode, { nodeId: rootId, depth: 0, rootId: rootId, visibleIds: visibleIds, collapsedIds: collapsedIds, dropTarget: dropTarget, selectedNodeId: selectedId, onToggleCollapse: toggleCollapse, onOpenContextMenu: openContextMenu, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onDragEnd: () => { setDraggedId(null); setDropTarget(null); }, renderTreeNodeMenu: renderTreeNodeMenu, onToggleDisabled: handleToggleDisabled, setSelectedId: setSelectedId }) })] }))] }), _jsx(TreeContextMenu, { contextMenu: contextMenu, onClose: closeContextMenu, children: (nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === rootId, close) })] }));
107
110
  }
108
111
  const TreeNode = memo(function TreeNode({ nodeId, depth, rootId, visibleIds, collapsedIds, dropTarget, selectedNodeId, onToggleCollapse, onOpenContextMenu, onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd, renderTreeNodeMenu, onToggleDisabled, setSelectedId, }) {
109
112
  var _a;
@@ -20,9 +20,10 @@ export declare function TreeContextMenu({ contextMenu, onClose, children, }: {
20
20
  onClose: () => void;
21
21
  children: (nodeId: string, onClose: () => void) => React.ReactNode;
22
22
  }): import("react").ReactPortal | null;
23
- export declare function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, onClose }: {
23
+ export declare function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, onImportPackedPrefab, onClose }: {
24
24
  getPrefab: () => Prefab;
25
25
  onReplacePrefab: (prefab: Prefab) => void;
26
26
  onImportPrefab: (prefab: Prefab) => void;
27
+ onImportPackedPrefab: (url: string) => void;
27
28
  onClose: () => void;
28
29
  }): import("react/jsx-runtime").JSX.Element;
@@ -13,7 +13,7 @@ import { createPortal } from 'react-dom';
13
13
  import { createEmptyPrefab } from './prefab';
14
14
  import { menu } from './styles';
15
15
  import { useEditorContext } from './PrefabEditor';
16
- import { loadJson, saveJson } from './utils';
16
+ import { loadJson, loadJsonFile, saveJson, withBasePath } from './utils';
17
17
  function MenuPanel({ children, style, }) {
18
18
  return (_jsx("div", { style: Object.assign(Object.assign(Object.assign({}, menu.container), { position: 'static' }), style), onClick: (e) => e.stopPropagation(), children: children }));
19
19
  }
@@ -80,8 +80,8 @@ export function TreeContextMenu({ contextMenu, onClose, children, }) {
80
80
  zIndex: 1000,
81
81
  }, onMouseLeave: onClose, onContextMenu: (e) => e.preventDefault(), children: children(contextMenu.nodeId, onClose) }), document.body);
82
82
  }
83
- export function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, onClose }) {
84
- const { onScreenshot, onExportGLB } = useEditorContext();
83
+ export function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, onImportPackedPrefab, onClose }) {
84
+ const { basePath, onScreenshot, onExportGLB } = useEditorContext();
85
85
  const handleNew = () => {
86
86
  onReplacePrefab(createEmptyPrefab());
87
87
  onClose();
@@ -98,10 +98,22 @@ export function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, onClose }
98
98
  onClose();
99
99
  };
100
100
  const handleImport = () => __awaiter(this, void 0, void 0, function* () {
101
- const loaded = yield loadJson();
101
+ const loaded = yield loadJsonFile();
102
102
  if (!loaded)
103
103
  return;
104
- onImportPrefab(loaded);
104
+ try {
105
+ const manifest = yield fetch(withBasePath(basePath, '/prefabs/manifest.json')).then(r => r.json());
106
+ const matched = manifest.find(entry => entry.endsWith(`/${loaded.filename}`) || entry === `/${loaded.filename}`);
107
+ if (matched) {
108
+ onImportPackedPrefab(matched);
109
+ onClose();
110
+ return;
111
+ }
112
+ }
113
+ catch (_a) {
114
+ // manifest not available, fall through to full import
115
+ }
116
+ onImportPrefab(loaded.prefab);
105
117
  onClose();
106
118
  });
107
119
  return (_jsxs(MenuPanel, { style: { overflow: 'visible' }, children: [_jsxs(MenuSubmenu, { label: "File", children: [_jsx(MenuItemButton, { onClick: handleNew, children: "New Prefab" }), _jsx(MenuItemButton, { onClick: handleOpen, children: "Open Prefab" }), _jsx(MenuItemButton, { onClick: handleImport, children: "Import Prefab" }), _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" })] })] }));
@@ -6,6 +6,7 @@ export type ContactEventPayload = {
6
6
  targetEntityId?: string | null;
7
7
  targetNodeId?: string | null;
8
8
  targetObject?: unknown;
9
+ collisionNormal?: [number, number, number];
9
10
  event?: unknown;
10
11
  };
11
12
  export type ClickEventPayload = {
@@ -2,7 +2,7 @@ import GameCanvas from "../../shared/GameCanvas";
2
2
  import type { Prefab } from "./types";
3
3
  import { PrefabEditorMode, type Scene } from "./PrefabRoot";
4
4
  import type { ExportGLBOptions } from "./utils";
5
- export declare function isAbsoluteAssetPath(path: string): boolean;
5
+ export { isExternalPath as isAbsoluteAssetPath } from "./utils";
6
6
  export declare function resolvePrefabAssetPath(basePath: string, file: string): string;
7
7
  export declare function getPrefabAssetRef(assetRef: string, folder: "models" | "textures" | "sound"): string;
8
8
  export interface PrefabEditorRef extends Scene {
@@ -21,6 +21,7 @@ export interface PrefabEditorRef extends Scene {
21
21
  export type { PrefabNode } from "./PrefabRoot";
22
22
  export interface EditorContextType {
23
23
  mode: PrefabEditorMode;
24
+ basePath: string;
24
25
  setMode: (mode: PrefabEditorMode) => void;
25
26
  transformMode: "translate" | "rotate" | "scale";
26
27
  setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
@@ -16,10 +16,11 @@ import { findComponentEntry } from "./types";
16
16
  import { PrefabEditorMode, PrefabRoot } from "./PrefabRoot";
17
17
  import EditorUI from "./EditorUI";
18
18
  import { base, toolbar } from "./styles";
19
- import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
19
+ import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, isExternalPath, regenerateIds, withBasePath } from "./utils";
20
20
  import { loadDroppedAssets } from "../dragdrop";
21
21
  import { denormalizePrefab, createImageNode, createModelNode, createNode } from './prefab';
22
22
  import { createPrefabStore, PrefabStoreProvider } from "./prefabStore";
23
+ import { decomposeModelToPrefabNodes, hasCollisionMeshConventions } from "./modelPrefab";
23
24
  function isObjectAttachedToRoot(root, object) {
24
25
  if (!root || !object)
25
26
  return false;
@@ -31,18 +32,12 @@ function isObjectAttachedToRoot(root, object) {
31
32
  }
32
33
  return false;
33
34
  }
34
- export function isAbsoluteAssetPath(path) {
35
- return (path.startsWith("data:") ||
36
- path.startsWith("http://") ||
37
- path.startsWith("https://"));
38
- }
35
+ export { isExternalPath as isAbsoluteAssetPath } from "./utils";
39
36
  export function resolvePrefabAssetPath(basePath, file) {
40
- if (isAbsoluteAssetPath(file))
41
- return file;
42
- return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
37
+ return withBasePath(basePath, file);
43
38
  }
44
39
  export function getPrefabAssetRef(assetRef, folder) {
45
- return isAbsoluteAssetPath(assetRef) ? assetRef : `${folder}/${assetRef}`;
40
+ return isExternalPath(assetRef) ? assetRef : `${folder}/${assetRef}`;
46
41
  }
47
42
  function SelectionHelper({ object }) {
48
43
  const objectRef = useRef(null);
@@ -76,7 +71,7 @@ const DEFAULT_PREFAB = {
76
71
  name: "New Prefab",
77
72
  root: createNode('Root', {}, { id: 'root' })
78
73
  };
79
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
74
+ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
80
75
  const [mode, setMode] = useState(initialMode);
81
76
  const [selectedId, setSelectedId] = useState(null);
82
77
  const [transformMode, setTransformMode] = useState("translate");
@@ -344,7 +339,23 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
344
339
  onModelLoaded: (model, filename, file) => {
345
340
  const path = getPrefabAssetRef(filename, 'models');
346
341
  scene === null || scene === void 0 ? void 0 : scene.addModel(path, model);
347
- add(createModelNode(path, file.name.replace(/\.[^.]+$/, '')));
342
+ const modelName = file.name.replace(/\.[^.]+$/, '');
343
+ const modelIdPrefix = modelName.replace(/[^\w-]+/g, '-') || 'model';
344
+ if (hasCollisionMeshConventions(model)) {
345
+ const textureRefs = new Map();
346
+ const decomposed = decomposeModelToPrefabNodes(model, {
347
+ idPrefix: modelIdPrefix,
348
+ getTexturePath: (texture, usage) => {
349
+ const key = `embedded/${modelIdPrefix}/${usage}/${texture.uuid}`;
350
+ textureRefs.set(key, texture);
351
+ return key;
352
+ },
353
+ });
354
+ textureRefs.forEach((texture, path) => scene === null || scene === void 0 ? void 0 : scene.addTexture(path, texture));
355
+ add(Object.assign(Object.assign({}, decomposed), { name: modelName || decomposed.name }));
356
+ return;
357
+ }
358
+ add(createModelNode(path, modelName));
348
359
  },
349
360
  onTextureLoaded: (texture, filename, file) => {
350
361
  const path = getPrefabAssetRef(filename, 'textures');
@@ -368,6 +379,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
368
379
  return getRoot();
369
380
  },
370
381
  mode,
382
+ basePath,
371
383
  get: getNode,
372
384
  getObject,
373
385
  getHandle,
@@ -382,7 +394,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
382
394
  addModel: (path, model) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addModel(path, model); },
383
395
  addTexture: (path, texture) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addTexture(path, texture); },
384
396
  addSound: (path, sound) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addSound(path, sound); },
385
- }), [add, duplicate, getHandle, getModel, getNode, getObject, getRoot, mode, move, remove, replace, replaceNode, update]);
397
+ }), [add, basePath, duplicate, getHandle, getModel, getNode, getObject, getRoot, mode, move, remove, replace, replaceNode, update]);
386
398
  const editorRefValue = useMemo(() => (Object.assign(Object.assign({}, sceneValue), { save: getPrefab, load: loadPrefab, undo,
387
399
  redo, screenshot: handleScreenshot, exportGLB: handleExportGLB, exportGLBData: handleExportGLBData, clearSelection })), [clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, redo, sceneValue, undo]);
388
400
  useImperativeHandle(ref, () => editorRefValue, [editorRefValue]);
@@ -394,6 +406,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode =
394
406
  }, [canvasProps]);
395
407
  return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsx(EditorRefContext.Provider, { value: editorRefValue, children: _jsxs(EditorContext.Provider, { value: {
396
408
  mode,
409
+ basePath,
397
410
  setMode: updateMode,
398
411
  transformMode,
399
412
  setTransformMode,
@@ -1,6 +1,6 @@
1
1
  import { Matrix4 } from "three";
2
2
  import type { Object3D, Texture } from "three";
3
- import type { ThreeEvent } from "@react-three/fiber";
3
+ import { type ThreeEvent } from "@react-three/fiber";
4
4
  import type { GameObject as GameObjectType, Prefab } from "./types";
5
5
  import type { LoadedModels } from "../dragdrop";
6
6
  import type { PrefabStoreApi } from "./prefabStore";
@@ -12,6 +12,7 @@ export type PrefabNode = Omit<GameObjectType, "children">;
12
12
  export interface Scene {
13
13
  root: Object3D | null;
14
14
  mode: PrefabEditorMode;
15
+ basePath: string;
15
16
  get(id: string): GameObjectType | null;
16
17
  getObject(id: string): Object3D | null;
17
18
  getHandle<T = unknown>(id: string, kind: string): T | null;
@@ -53,5 +54,6 @@ interface RendererProps {
53
54
  editMode?: boolean;
54
55
  parentMatrix?: Matrix4;
55
56
  isVisible?: boolean;
57
+ basePath?: string;
56
58
  }
57
59
  export default PrefabRoot;
@@ -1,3 +1,12 @@
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
  var __rest = (this && this.__rest) || function (s, e) {
2
11
  var t = {};
3
12
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -12,6 +21,7 @@ var __rest = (this && this.__rest) || function (s, e) {
12
21
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
22
  import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
14
23
  import { Euler, Matrix4 } from "three";
24
+ import { useThree } from "@react-three/fiber";
15
25
  import { useStore } from "zustand";
16
26
  import { useClickValid } from "./useClickValid";
17
27
  import { findComponent, getNodeUserData } from "./types";
@@ -19,7 +29,7 @@ import { getComponentDef, registerComponent } from "./components/ComponentRegist
19
29
  import { builtinComponents } from "./components";
20
30
  import { loadModel, loadSound, loadTexture } from "../dragdrop";
21
31
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties } from "./InstanceProvider";
22
- import { composeTransform, decompose } from "./utils";
32
+ import { composeTransform, decompose, withBasePath } from "./utils";
23
33
  import { createPrefabStore, PrefabStoreProvider, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
24
34
  import { AssetRuntimeContext, NodeScope } from "./assetRuntime";
25
35
  import { gameEvents } from "./GameEvents";
@@ -37,13 +47,19 @@ const EMPTY_NODE_COMPONENTS = {
37
47
  clickEventName: null,
38
48
  composition: [],
39
49
  };
40
- /** Resolve a relative or absolute asset file path against a base path. */
41
- function resolveAssetPath(basePath, file) {
42
- if (file.startsWith("data:"))
43
- return file;
44
- if (file.startsWith("http://") || file.startsWith("https://"))
45
- return file;
46
- return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
50
+ function precompileModel(model, renderer, camera) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ try {
53
+ if (typeof renderer.compileAsync === "function") {
54
+ yield renderer.compileAsync(model, camera);
55
+ return;
56
+ }
57
+ renderer.compile(model, camera);
58
+ }
59
+ catch (error) {
60
+ console.warn("Failed to precompile model before adding it to the scene", error);
61
+ }
62
+ });
47
63
  }
48
64
  /** Check if a model component's assets are loaded. */
49
65
  function isNodeReady(model, loadedModels) {
@@ -74,6 +90,8 @@ export function useScene() {
74
90
  return scene;
75
91
  }
76
92
  export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onEditNodeClick, basePath = "", children }, ref) => {
93
+ const renderer = useThree(state => state.gl);
94
+ const camera = useThree(state => state.camera);
77
95
  const [models, setModels] = useState({});
78
96
  const [textures, setTextures] = useState({});
79
97
  const [sounds, setSounds] = useState({});
@@ -84,6 +102,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
84
102
  const failedModels = useRef(new Set());
85
103
  const failedTextures = useRef(new Set());
86
104
  const failedSounds = useRef(new Set());
105
+ const injectedModelVersions = useRef({});
87
106
  const objectRefs = useRef({});
88
107
  const nodeHandles = useRef(new Map());
89
108
  const [ownedStore] = useState(() => {
@@ -140,6 +159,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
140
159
  return (_a = objectRefs.current[rootId]) !== null && _a !== void 0 ? _a : null;
141
160
  },
142
161
  mode: editMode ? PrefabEditorMode.Edit : PrefabEditorMode.Play,
162
+ basePath,
143
163
  get: getNode,
144
164
  getObject,
145
165
  getHandle,
@@ -155,13 +175,22 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
155
175
  duplicate: (id) => resolvedStore.getState().duplicateNode(id),
156
176
  move: (draggedId, targetId, position) => resolvedStore.getState().moveNode(draggedId, targetId, position),
157
177
  replace: (prefab) => resolvedStore.getState().replacePrefab(prefab),
158
- addModel: (path, model) => setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model }))),
178
+ addModel: (path, model) => {
179
+ var _a;
180
+ const version = ((_a = injectedModelVersions.current[path]) !== null && _a !== void 0 ? _a : 0) + 1;
181
+ injectedModelVersions.current[path] = version;
182
+ void precompileModel(model, renderer, camera).then(() => {
183
+ if (injectedModelVersions.current[path] !== version)
184
+ return;
185
+ setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
186
+ });
187
+ },
159
188
  addTexture: (path, texture) => setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture }))),
160
189
  addSound: (path, sound) => {
161
190
  soundManager.setBuffer(path, sound);
162
191
  setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
163
192
  },
164
- }), [editMode, getHandle, getModel, getNode, getObject, resolvedStore, rootId]);
193
+ }), [basePath, camera, editMode, getHandle, getModel, getNode, getObject, renderer, resolvedStore, rootId]);
165
194
  useImperativeHandle(ref, () => sceneValue, [sceneValue]);
166
195
  const registerRef = useCallback((id, obj) => {
167
196
  objectRefs.current[id] = obj;
@@ -176,7 +205,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
176
205
  if (loaded[file] || injected[file] || loading.current.has(file) || failed.has(file))
177
206
  return;
178
207
  loading.current.add(file);
179
- void loader(resolveAssetPath(basePath, file)).then(result => {
208
+ void loader(withBasePath(basePath, file)).then(result => {
180
209
  loading.current.delete(file);
181
210
  if (!result.success) {
182
211
  console.warn(`Failed to load asset: ${file}`, result.error);
@@ -189,13 +218,14 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
189
218
  const type = entry.slice(0, separator);
190
219
  const file = entry.slice(separator + 1);
191
220
  if (type === 'model') {
192
- loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then(result => {
221
+ loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then((result) => __awaiter(void 0, void 0, void 0, function* () {
193
222
  const loadedModel = result.model;
194
223
  if (result.success && loadedModel) {
224
+ yield precompileModel(loadedModel, renderer, camera);
195
225
  setModels(currentModels => (Object.assign(Object.assign({}, currentModels), { [file]: loadedModel })));
196
226
  }
197
227
  return result;
198
- }));
228
+ })));
199
229
  }
200
230
  else if (type === 'texture') {
201
231
  loadAsset(file, textures, injectedTextures, failedTextures.current, path => loadTexture(path).then(result => {
@@ -217,7 +247,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
217
247
  }));
218
248
  }
219
249
  });
220
- }, [assetRefCounts, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
250
+ }, [assetRefCounts, basePath, camera, injectedModels, injectedSounds, injectedTextures, models, renderer, sounds, textures]);
221
251
  const assetRuntime = useMemo(() => ({
222
252
  registerHandle,
223
253
  getHandle,
@@ -235,7 +265,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
235
265
  emitNodePointerEvent(clickEventName, event, nodeId, node, fallbackObject);
236
266
  onClick === null || onClick === void 0 ? void 0 : onClick(event, node);
237
267
  }, [onClick, resolvedStore]);
238
- const content = (_jsxs(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, registerRef: registerRef, children: [_jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, onEditNodeClick: editMode ? onEditNodeClick : undefined, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY }), children] }));
268
+ const content = (_jsxs(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, registerRef: registerRef, children: [_jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, onEditNodeClick: editMode ? onEditNodeClick : undefined, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY, basePath: basePath }), children] }));
239
269
  const runtimeContent = (_jsx(SceneContext.Provider, { value: sceneValue, children: _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }) }));
240
270
  return _jsx(PrefabStoreProvider, { store: resolvedStore, children: runtimeContent });
241
271
  });
@@ -303,10 +333,7 @@ function analyzeNodeComponents(node) {
303
333
  }
304
334
  function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
305
335
  var _a;
306
- const trimmedEventName = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
307
- if (!trimmedEventName)
308
- return;
309
- gameEvents.emit(trimmedEventName, {
336
+ const payload = {
310
337
  sourceEntityId: nodeId,
311
338
  sourceNodeId: nodeId,
312
339
  nodeId,
@@ -319,7 +346,12 @@ function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
319
346
  metaKey: event.nativeEvent.metaKey,
320
347
  shiftKey: event.nativeEvent.shiftKey,
321
348
  r3fEvent: event,
322
- });
349
+ };
350
+ gameEvents.emit('click', payload);
351
+ const trimmedEventName = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
352
+ if (!trimmedEventName)
353
+ return;
354
+ gameEvents.emit(trimmedEventName, payload);
323
355
  }
324
356
  export function GameObjectRenderer(props) {
325
357
  var _a, _b;
@@ -374,7 +406,7 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
374
406
  }
375
407
  return _jsx(_Fragment, { children: renderedInstances });
376
408
  }
377
- function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, }) {
409
+ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, basePath = "", }) {
378
410
  const gameObject = usePrefabNode(nodeId);
379
411
  const childIds = usePrefabChildIds(nodeId);
380
412
  const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
@@ -410,9 +442,13 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick,
410
442
  rotation: transform.rotation,
411
443
  scale: transform.scale,
412
444
  };
445
+ const worldTransform = decompose(world);
413
446
  const groupProps = Object.assign(Object.assign({}, metadataProps), transformProps);
414
- const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, onEditNodeClick: onEditNodeClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode, isVisible: nodeVisible });
415
- const inner = renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes);
447
+ const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, onEditNodeClick: onEditNodeClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode, isVisible: nodeVisible, basePath: basePath });
448
+ const nodeInteractionHandlers = editMode ? editClickHandlers : primaryClickHandlers;
449
+ const componentRuntimeProps = Object.assign(Object.assign({ editMode,
450
+ nodeInteractionHandlers }, transformProps), { worldPosition: worldTransform.position });
451
+ const inner = renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes, basePath, componentRuntimeProps);
416
452
  const editAnchor = editMode ? (_jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) })) : null;
417
453
  const standardNode = (_jsxs("group", Object.assign({ ref: handleGroupRef }, groupProps, { visible: nodeVisible }, (editMode ? editClickHandlers : undefined), { children: [editAnchor, inner] })));
418
454
  return (_jsx(NodeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: standardNode }));
@@ -485,7 +521,7 @@ function getNodeTransformProps(node) {
485
521
  scale: (_d = t === null || t === void 0 ? void 0 : t.scale) !== null && _d !== void 0 ? _d : [1, 1, 1],
486
522
  };
487
523
  }
488
- function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes) {
524
+ function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes, basePath = "", componentRuntimeProps) {
489
525
  var _a, _b, _c, _d, _e, _f;
490
526
  const geometry = analyzedComponents.geometry;
491
527
  const models = analyzedComponents.models;
@@ -550,7 +586,7 @@ function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandler
550
586
  }
551
587
  let content = _jsxs(_Fragment, { children: [primaryContent, contentChildren] });
552
588
  for (const { key, View, properties } of analyzedComponents.composition) {
553
- content = (_jsx(View, { properties: properties, children: content }, key));
589
+ content = (_jsx(View, Object.assign({ properties: properties, basePath: basePath }, componentRuntimeProps, { children: content }), key));
554
590
  }
555
591
  return content;
556
592
  }
@@ -1,4 +1,5 @@
1
1
  import type { FC } from "react";
2
+ import type { ThreeEvent } from "@react-three/fiber";
2
3
  import type { ComponentData, GameObject } from "../types";
3
4
  export type AssetRef = {
4
5
  type: "model" | "texture" | "sound";
@@ -12,13 +13,27 @@ export interface ComponentViewProps<P = Record<string, unknown>> {
12
13
  properties: P;
13
14
  /** Children to render for components that wrap the current subtree. */
14
15
  children?: React.ReactNode;
16
+ /** Whether this node is currently rendered in editor mode. */
17
+ editMode?: boolean;
18
+ /** Node-level pointer/click handlers for custom components that render their own pickable objects. */
19
+ nodeInteractionHandlers?: NodeInteractionHandlers;
15
20
  /** Current node local position for wrapper components. */
16
21
  position?: [number, number, number];
17
22
  /** Current node local rotation in radians for wrapper components. */
18
23
  rotation?: [number, number, number];
19
24
  /** Current node local scale for wrapper components. */
20
25
  scale?: [number, number, number];
26
+ /** Current node world position. Components that create world-space resources should prefer this. */
27
+ worldPosition?: [number, number, number];
28
+ /** Public asset URL prefix, such as a Next.js basePath. */
29
+ basePath?: string;
21
30
  }
31
+ export type NodeInteractionHandlers = {
32
+ onClick?: (event: ThreeEvent<PointerEvent>) => void;
33
+ onPointerDown?: (event: ThreeEvent<PointerEvent>) => void;
34
+ onPointerMove?: (event: ThreeEvent<PointerEvent>) => void;
35
+ onPointerUp?: (event: ThreeEvent<PointerEvent>) => void;
36
+ };
22
37
  export interface Component {
23
38
  name: string;
24
39
  /** Set when this component occupies a single slot on a node. Use a string to share a slot across component types. */
@@ -0,0 +1,3 @@
1
+ import type { Component } from './ComponentRegistry';
2
+ declare const PrefabRefComponent: Component;
3
+ export default PrefabRefComponent;