react-three-game 0.0.109 → 0.0.110

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 (36) hide show
  1. package/README.md +4 -11
  2. package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +46 -8
  3. package/dist/plugins/crashcat/CrashcatRagdoll.d.ts +1 -1
  4. package/dist/plugins/crashcat/CrashcatRuntime.d.ts +1 -1
  5. package/dist/shared/ContactShadow.d.ts +1 -1
  6. package/dist/shared/GameCanvas.d.ts +1 -1
  7. package/dist/tools/assetviewer/page.d.ts +10 -10
  8. package/dist/tools/dragdrop/DragDropLoader.d.ts +2 -2
  9. package/dist/tools/prefabeditor/Dropdown.d.ts +1 -1
  10. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -1
  11. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +2 -2
  12. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -1
  13. package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -1
  14. package/dist/tools/prefabeditor/PrefabEditor.js +67 -51
  15. package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -2
  16. package/dist/tools/prefabeditor/PrefabRoot.js +73 -208
  17. package/dist/tools/prefabeditor/SceneProvider.d.ts +14 -0
  18. package/dist/tools/prefabeditor/SceneProvider.js +68 -0
  19. package/dist/tools/prefabeditor/assetRuntime.d.ts +25 -2
  20. package/dist/tools/prefabeditor/assetRuntime.js +115 -1
  21. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +3 -3
  22. package/dist/tools/prefabeditor/components/Input.d.ts +19 -19
  23. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -1
  24. package/dist/tools/prefabeditor/components/MaterialComponent.js +13 -14
  25. package/dist/tools/prefabeditor/components/ModelComponent.js +2 -3
  26. package/dist/tools/prefabeditor/components/PrefabRefComponent.js +39 -15
  27. package/dist/tools/prefabeditor/components/SoundComponent.js +6 -2
  28. package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -5
  29. package/dist/tools/prefabeditor/components/lightUtils.d.ts +2 -2
  30. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  31. package/dist/tools/prefabeditor/prefabStore.js +11 -0
  32. package/dist/viewer.d.ts +11 -12
  33. package/dist/viewer.js +7 -9
  34. package/package.json +9 -9
  35. package/dist/tools/prefabeditor/components/runtime.d.ts +0 -4
  36. package/dist/tools/prefabeditor/components/runtime.js +0 -372
package/README.md CHANGED
@@ -153,6 +153,7 @@ interface GameObject {
153
153
  name?: string;
154
154
  disabled?: boolean;
155
155
  locked?: boolean;
156
+ hidden?: boolean;
156
157
  components?: Record<string, { type: string; properties: any }>;
157
158
  children?: GameObject[];
158
159
  }
@@ -280,17 +281,9 @@ Custom component `View`s use normal React and R3F behavior — `useFrame`, refs,
280
281
 
281
282
  ## Useful Exports
282
283
 
283
- * `GameCanvas`, `PrefabRoot`, `PrefabEditor`, `PrefabEditorMode`
284
- * `Prefab`, `GameObject`, `ComponentData`, `PrefabNode`, `PrefabEditorRef`, `Scene`
285
- * `registerComponent`, `Component`, `ComponentViewProps`, `FieldDefinition`
286
- * `useScene`, `useEditorRef`, `useEditorContext`
287
- * `useNode`, `useNodeObject`, `useNodeHandle`, `useAssetRuntime`
288
- * `usePrefabStore`, `usePrefabStoreApi`
289
- * `gameEvents`, `useGameEvent`, `useClickEvent`
290
- * `loadJson`, `saveJson`, `loadFiles`, `loadModel`, `loadTexture`, `loadSound`
291
- * `exportGLB`, `exportGLBData`, `regenerateIds`, `computeParentWorldMatrix`
292
- * `ground`, `soundManager`
293
- * `FieldRenderer`, `Vector3Field`, `NumberField`, `StringField`, `BooleanField`, `SelectField`, `ColorField`
284
+ * `react-three-game/viewer`: `GameCanvas`, `PrefabRoot`, `PrefabEditorMode`, `registerComponent`, scene hooks, event hooks, asset/runtime hooks, prefab types, loaders, `ground`, and `soundManager`
285
+ * `react-three-game/editor`: everything from `/viewer`, plus `PrefabEditor`, editor refs/context, prefab store hooks, inspector field components, JSON import/export helpers, GLB export helpers, material overrides, model decomposition, asset viewer components, and `three/tsl` helpers
286
+ * `react-three-game/plugins/crashcat`: `CrashcatRuntime`, `CrashcatPhysicsComponent`, `CrashcatRagdollComponent`, ragdoll helpers, static body helpers, and `useCrashcat`
294
287
 
295
288
  ## Development
296
289
 
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useFrame } from "@react-three/fiber";
4
- import { useEffect, useMemo, useRef } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef } from "react";
5
+ import { useStore } from "zustand";
5
6
  import { BooleanField, FieldRenderer, StringField, Vector3Field, } from "../../tools/prefabeditor/components/Input";
6
- import { useAssetRuntime, useNode } from "../../tools/prefabeditor/assetRuntime";
7
+ import { useModelAsset, useNode } from "../../tools/prefabeditor/assetRuntime";
7
8
  import { usePrefabStoreApi } from "../../tools/prefabeditor/prefabStore";
8
9
  import { PrefabEditorMode, useScene } from "../../tools/prefabeditor/SceneContext";
9
10
  import { box, capsule, convexHull, MotionQuality, MotionType, rigidBody, sphere, triangleMesh, } from "crashcat";
@@ -51,7 +52,26 @@ const scratchBoundsSize = new Vector3();
51
52
  const worldQuaternion = new Quaternion();
52
53
  const parentWorldQuaternion = new Quaternion();
53
54
  const localQuaternion = new Quaternion();
55
+ // Extracted collider geometry is keyed by the sequence of geometry UUIDs in the
56
+ // object's subtree. Clones of the same model share geometry instances and have
57
+ // identical model-internal transforms, and the extraction is expressed in
58
+ // object-local space (the object's own world transform is divided out), so the
59
+ // result is identical across every instance/body of that model. Extracting once
60
+ // avoids re-walking thousands of vertices per rigid body.
61
+ const geometryDataCache = new Map();
54
62
  function collectGeometryData(object) {
63
+ let cacheKey = "";
64
+ object.traverse((child) => {
65
+ var _a;
66
+ const geometry = child.geometry;
67
+ if (((_a = geometry === null || geometry === void 0 ? void 0 : geometry.attributes) === null || _a === void 0 ? void 0 : _a.position) && geometry.uuid)
68
+ cacheKey += `${geometry.uuid};`;
69
+ });
70
+ if (cacheKey) {
71
+ const cached = geometryDataCache.get(cacheKey);
72
+ if (cached)
73
+ return cached;
74
+ }
55
75
  const positions = [];
56
76
  const indices = [];
57
77
  let vertexOffset = 0;
@@ -83,7 +103,10 @@ function collectGeometryData(object) {
83
103
  });
84
104
  if (positions.length === 0 || indices.length < 3)
85
105
  return null;
86
- return { positions, indices };
106
+ const result = { positions, indices };
107
+ if (cacheKey)
108
+ geometryDataCache.set(cacheKey, result);
109
+ return result;
87
110
  }
88
111
  function createShapeForObject(object, physics) {
89
112
  var _a, _b;
@@ -246,8 +269,23 @@ function CrashcatPhysicsView({ properties, children }) {
246
269
  const scene = useScene();
247
270
  const store = usePrefabStoreApi();
248
271
  const api = useCrashcat();
249
- const { getAssetRevision } = useAssetRuntime();
250
- const revision = getAssetRevision();
272
+ // Subscribe only to this node's Model filename (not its full node, which would
273
+ // re-render on every transform edit), then to that one model's loaded asset.
274
+ // Colliders rebuild when *this* node's mesh loads, not when any asset loads.
275
+ const modelPath = useStore(store, useCallback((s) => {
276
+ var _a, _b;
277
+ const node = s.nodesById[nodeId];
278
+ if (!node)
279
+ return null;
280
+ for (const key in node.components) {
281
+ const component = node.components[key];
282
+ if ((component === null || component === void 0 ? void 0 : component.type) === "Model") {
283
+ return (_b = (_a = component.properties) === null || _a === void 0 ? void 0 : _a.filename) !== null && _b !== void 0 ? _b : null;
284
+ }
285
+ }
286
+ return null;
287
+ }, [nodeId]));
288
+ const loadedModel = useModelAsset(modelPath);
251
289
  const bodyRef = useRef(null);
252
290
  const motionTypeRef = useRef(MotionType.STATIC);
253
291
  const needsRegistrationRef = useRef(false);
@@ -307,8 +345,8 @@ function CrashcatPhysicsView({ properties, children }) {
307
345
  lastQuaternionRef.current = null;
308
346
  };
309
347
  useEffect(() => {
310
- // Rebuild mesh-derived colliders when referenced assets finish loading.
311
- void revision;
348
+ // Rebuild mesh-derived colliders when this node's referenced model finishes loading.
349
+ void loadedModel;
312
350
  needsRegistrationRef.current = true;
313
351
  if (api) {
314
352
  api.unregister(nodeId);
@@ -325,7 +363,7 @@ function CrashcatPhysicsView({ properties, children }) {
325
363
  getObject,
326
364
  nodeId,
327
365
  physics,
328
- revision,
366
+ loadedModel,
329
367
  ]);
330
368
  useFrame(() => {
331
369
  if (needsRegistrationRef.current) {
@@ -52,7 +52,7 @@ export type CrashcatRagdollProps = {
52
52
  nodeInteractionHandlers?: NodeInteractionHandlers;
53
53
  };
54
54
  export declare function createRagdollSettings(scale?: number, angleA?: number, angleB?: number, twistAngle?: number): RagdollSettings;
55
- export declare function CrashcatRagdoll({ position, scale, swingAngle, shoulderAngle, twistAngle, stabilize, initialLinearVelocity, initialAngularVelocity, color, clickImpulse, nodeInteractionHandlers, }: CrashcatRagdollProps): import("react/jsx-runtime").JSX.Element;
55
+ export declare function CrashcatRagdoll({ position, scale, swingAngle, shoulderAngle, twistAngle, stabilize, initialLinearVelocity, initialAngularVelocity, color, clickImpulse, nodeInteractionHandlers, }: CrashcatRagdollProps): import("react").JSX.Element;
56
56
  declare const CrashcatRagdollComponent: Component;
57
57
  export default CrashcatRagdollComponent;
58
58
  export declare function createStaticBoxBody(world: World, objectLayer: number, halfExtents: Vec3, position: Vec3): rigidBody.RigidBody;
@@ -24,4 +24,4 @@ export declare function useCrashcat(): CrashcatApi | null;
24
24
  export declare function CrashcatRuntime({ debug, children }: {
25
25
  debug?: boolean;
26
26
  children?: React.ReactNode;
27
- }): import("react/jsx-runtime").JSX.Element;
27
+ }): import("react").JSX.Element;
@@ -4,5 +4,5 @@ interface ContactShadowProps {
4
4
  scale?: number;
5
5
  yOffset?: number;
6
6
  }
7
- declare const ContactShadow: ({ opacity, blur, scale, yOffset, }: ContactShadowProps) => import("react/jsx-runtime").JSX.Element;
7
+ declare const ContactShadow: ({ opacity, blur, scale, yOffset, }: ContactShadowProps) => import("react").JSX.Element;
8
8
  export default ContactShadow;
@@ -5,4 +5,4 @@ export interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
5
5
  children: React.ReactNode;
6
6
  glConfig?: WebGPURendererParameters;
7
7
  }
8
- export default function GameCanvas({ loader, children, glConfig, onCreated, style, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
8
+ export default function GameCanvas({ loader, children, glConfig, onCreated, style, ...props }: GameCanvasProps): import("react").JSX.Element;
@@ -4,48 +4,48 @@ interface TextureListViewerProps {
4
4
  onSelect: (file: string) => void;
5
5
  basePath?: string;
6
6
  }
7
- export declare function TextureListViewer({ files, selected, onSelect, basePath }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function TextureListViewer({ files, selected, onSelect, basePath }: TextureListViewerProps): import("react").JSX.Element;
8
8
  interface ModelListViewerProps {
9
9
  files: string[];
10
10
  selected?: string;
11
11
  onSelect: (file: string) => void;
12
12
  basePath?: string;
13
13
  }
14
- export declare function ModelListViewer({ files, selected, onSelect, basePath }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function ModelListViewer({ files, selected, onSelect, basePath }: ModelListViewerProps): import("react").JSX.Element;
15
15
  interface SoundListViewerProps {
16
16
  files: string[];
17
17
  selected?: string;
18
18
  onSelect: (file: string) => void;
19
19
  basePath?: string;
20
20
  }
21
- export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react").JSX.Element;
22
22
  export declare function TexturePicker({ value, onChange, basePath }: {
23
23
  value: string | undefined;
24
24
  onChange: (value: string | undefined) => void;
25
25
  basePath?: string;
26
- }): import("react/jsx-runtime").JSX.Element;
26
+ }): import("react").JSX.Element;
27
27
  export declare function ModelPicker({ value, onChange, basePath, pickerKey }: {
28
28
  value: string | undefined;
29
29
  onChange: (value: string | undefined) => void;
30
30
  basePath?: string;
31
31
  pickerKey?: string;
32
- }): import("react/jsx-runtime").JSX.Element;
32
+ }): import("react").JSX.Element;
33
33
  export declare function SoundPicker({ value, onChange, basePath }: {
34
34
  value: string | undefined;
35
35
  onChange: (value: string | undefined) => void;
36
36
  basePath?: string;
37
- }): import("react/jsx-runtime").JSX.Element;
37
+ }): import("react").JSX.Element;
38
38
  export declare function SingleTextureViewer({ file, basePath }: {
39
39
  file?: string;
40
40
  basePath?: string;
41
- }): import("react/jsx-runtime").JSX.Element;
41
+ }): import("react").JSX.Element;
42
42
  export declare function SingleModelViewer({ file, basePath }: {
43
43
  file?: string;
44
44
  basePath?: string;
45
- }): import("react/jsx-runtime").JSX.Element;
45
+ }): import("react").JSX.Element;
46
46
  export declare function SingleSoundViewer({ file, basePath }: {
47
47
  file?: string;
48
48
  basePath?: string;
49
- }): import("react/jsx-runtime").JSX.Element | null;
50
- export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
49
+ }): import("react").JSX.Element | null;
50
+ export declare function SharedCanvas(): import("react").JSX.Element;
51
51
  export {};
@@ -21,6 +21,6 @@ export declare function loadDroppedAssets(dataTransfer: DataTransfer | null, opt
21
21
  export declare function loadUrls(urls: string[], options: AssetLoadOptions): Promise<void>;
22
22
  export declare function loadUrl(url: string, options: AssetLoadOptions): Promise<void>;
23
23
  export declare function loadFiles(files: File[], { onModelLoaded, onTextureLoaded, onSoundLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions): Promise<void>;
24
- export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react/jsx-runtime").JSX.Element;
25
- export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react").JSX.Element;
25
+ export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react").JSX.Element;
26
26
  export {};
@@ -11,5 +11,5 @@ export declare function Dropdown({ trigger, children, placement, offset, zIndex,
11
11
  placement?: Placement;
12
12
  offset?: number;
13
13
  zIndex?: number;
14
- }): import("react/jsx-runtime").JSX.Element;
14
+ }): import("react").JSX.Element;
15
15
  export {};
@@ -9,4 +9,4 @@ export default function EditorTree({ selectedId, setSelectedId, getPrefab, onRep
9
9
  onRedo?: () => void;
10
10
  canUndo?: boolean;
11
11
  canRedo?: boolean;
12
- }): import("react/jsx-runtime").JSX.Element;
12
+ }): import("react").JSX.Element;
@@ -14,7 +14,7 @@ export declare function TreeNodeMenu({ isRoot, nodeId, locked, onAddChild, onFoc
14
14
  onDuplicate?: (nodeId: string) => void;
15
15
  onDelete?: (nodeId: string) => void;
16
16
  onClose: () => void;
17
- }): import("react/jsx-runtime").JSX.Element;
17
+ }): import("react").JSX.Element;
18
18
  export declare function TreeContextMenu({ contextMenu, onClose, children, }: {
19
19
  contextMenu: TreeContextMenuState;
20
20
  onClose: () => void;
@@ -26,4 +26,4 @@ export declare function FileMenu({ getPrefab, onReplacePrefab, onImportPrefab, o
26
26
  onImportPrefab: (prefab: Prefab) => void;
27
27
  onImportPackedPrefab: (url: string) => void;
28
28
  onClose: () => void;
29
- }): import("react/jsx-runtime").JSX.Element;
29
+ }): import("react").JSX.Element;
@@ -10,5 +10,5 @@ declare function EditorUI({ selectedId, setSelectedId, getPrefab, onReplacePrefa
10
10
  onRedo?: () => void;
11
11
  canUndo?: boolean;
12
12
  canRedo?: boolean;
13
- }): import("react/jsx-runtime").JSX.Element;
13
+ }): import("react").JSX.Element;
14
14
  export default EditorUI;
@@ -29,7 +29,7 @@ export declare function GameInstanceProvider({ children, models, onSelect, onCli
29
29
  registerRef?: (id: string, obj: Object3D | null) => void;
30
30
  selectedId?: string | null;
31
31
  editMode?: boolean;
32
- }): import("react/jsx-runtime").JSX.Element;
32
+ }): import("react").JSX.Element;
33
33
  export declare function useInstanceCheck(id: string): boolean;
34
34
  export declare function GameInstance({ id, sourceId, modelUrl, locked, position, rotation, scale, visible, onClick: _onClick, }: {
35
35
  id: string;
@@ -12,7 +12,7 @@ import { MapControls, TransformControls, useHelper } from "@react-three/drei";
12
12
  import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
13
13
  import { BoxHelper } from "three";
14
14
  import { findComponentEntry } from "./types";
15
- import { GameCanvas, PrefabRoot, PrefabEditorMode, SceneContext, createImageNode, createModelNode, denormalizePrefab } from "../../viewer";
15
+ import { GameCanvas, PrefabRoot, PrefabEditorMode, SceneContext, AssetRuntimeProvider, createImageNode, createModelNode, denormalizePrefab } from "../../viewer";
16
16
  import EditorUI from "./EditorUI";
17
17
  import { base, toolbar } from "./styles";
18
18
  import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, isExternalPath, regenerateIds, withBasePath } from "./utils";
@@ -65,11 +65,12 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
65
65
  const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
66
66
  const startingPrefab = initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : DEFAULT_PREFAB;
67
67
  const [prefabStore] = useState(() => createPrefabStore(startingPrefab));
68
- const [history, setHistory] = useState([startingPrefab]);
68
+ const [history, setHistory] = useState(() => [prefabStore.getState()]);
69
69
  const [historyIndex, setHistoryIndex] = useState(0);
70
70
  const historyIndexRef = useRef(0);
71
71
  const historyTimeoutRef = useRef(null);
72
- const prefabRootRef = useRef(null);
72
+ const notifyRafRef = useRef(null);
73
+ const runtimeRef = useRef(null);
73
74
  const canvasRef = useRef(null);
74
75
  const controlsRef = useRef(null);
75
76
  const transformControlsRef = useRef(null);
@@ -81,11 +82,14 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
81
82
  }, []);
82
83
  const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
83
84
  const getNode = useCallback((nodeId) => { var _a; return (_a = prefabStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null; }, [prefabStore]);
84
- const getRoot = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
85
- const getObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
86
- const getHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getHandle(nodeId, kind)) !== null && _b !== void 0 ? _b : null; }, []);
87
- const getModel = useCallback((path) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getModel(path)) !== null && _b !== void 0 ? _b : null; }, []);
88
- const scheduleHistory = useCallback((nextPrefab) => {
85
+ const getRoot = useCallback(() => { var _a, _b; return (_b = (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.getObject(prefabStore.getState().rootId)) !== null && _b !== void 0 ? _b : null; }, [prefabStore]);
86
+ const getObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
87
+ const getHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.getHandle(nodeId, kind)) !== null && _b !== void 0 ? _b : null; }, []);
88
+ const getModel = useCallback((path) => { var _a, _b; return (_b = (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.getModel(path)) !== null && _b !== void 0 ? _b : null; }, []);
89
+ // History stores normalized state snapshots. Because store mutations use
90
+ // structural sharing (unchanged nodes keep their references), capturing a
91
+ // snapshot is O(1) instead of deep-cloning the whole prefab tree.
92
+ const scheduleHistory = useCallback((snapshot) => {
89
93
  if (historyTimeoutRef.current) {
90
94
  clearTimeout(historyTimeoutRef.current);
91
95
  historyTimeoutRef.current = null;
@@ -93,7 +97,7 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
93
97
  historyTimeoutRef.current = setTimeout(() => {
94
98
  const currentHistoryIndex = historyIndexRef.current;
95
99
  setHistory(prev => {
96
- const nextHistory = [...prev.slice(0, currentHistoryIndex + 1), nextPrefab];
100
+ const nextHistory = [...prev.slice(0, currentHistoryIndex + 1), snapshot];
97
101
  return nextHistory.length > MAX_HISTORY_LENGTH ? nextHistory.slice(1) : nextHistory;
98
102
  });
99
103
  const nextHistoryIndex = Math.min(currentHistoryIndex + 1, MAX_HISTORY_LENGTH - 1);
@@ -102,19 +106,29 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
102
106
  historyTimeoutRef.current = null;
103
107
  }, HISTORY_DEBOUNCE_MS);
104
108
  }, []);
109
+ // Coalesce onChange notifications to once per frame. denormalizePrefab walks
110
+ // the entire tree, so calling it on every mutation (e.g. per-frame gizmo
111
+ // drags) does not scale. We only pay that cost once, with the latest state.
112
+ const scheduleChange = useCallback(() => {
113
+ if (!onChangeRef.current || notifyRafRef.current != null)
114
+ return;
115
+ notifyRafRef.current = requestAnimationFrame(() => {
116
+ var _a;
117
+ notifyRafRef.current = null;
118
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, denormalizePrefab(prefabStore.getState()));
119
+ });
120
+ }, [prefabStore]);
105
121
  const mutate = useCallback((run, pushHistory = isEditMode) => {
106
- var _a;
107
122
  const before = prefabStore.getState();
108
123
  const result = run(before);
109
124
  const after = prefabStore.getState();
110
125
  if (after === before)
111
126
  return result;
112
- const prefab = denormalizePrefab(after);
113
- (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, prefab);
127
+ scheduleChange();
114
128
  if (pushHistory)
115
- scheduleHistory(prefab);
129
+ scheduleHistory(after);
116
130
  return result;
117
- }, [isEditMode, prefabStore, scheduleHistory]);
131
+ }, [isEditMode, prefabStore, scheduleChange, scheduleHistory]);
118
132
  const update = useCallback((id, fn) => {
119
133
  mutate(s => s.updateNode(id, fn));
120
134
  }, [mutate]);
@@ -172,7 +186,7 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
172
186
  }
173
187
  if (options === null || options === void 0 ? void 0 : options.resetHistory) {
174
188
  setSelectedId(null);
175
- setHistory([prefab]);
189
+ setHistory([prefabStore.getState()]);
176
190
  historyIndexRef.current = 0;
177
191
  setHistoryIndex(0);
178
192
  }
@@ -189,6 +203,9 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
189
203
  if (historyTimeoutRef.current) {
190
204
  clearTimeout(historyTimeoutRef.current);
191
205
  }
206
+ if (notifyRafRef.current != null) {
207
+ cancelAnimationFrame(notifyRafRef.current);
208
+ }
192
209
  };
193
210
  }, []);
194
211
  useEffect(() => {
@@ -210,14 +227,13 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
210
227
  add(regenerateIds(prefab.root));
211
228
  }, [add]);
212
229
  const applyHistory = useCallback((index) => {
213
- var _a;
214
230
  detachTransformControls();
215
- prefabStore.getState().replacePrefab(history[index]);
216
- (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, history[index]);
231
+ prefabStore.getState().restoreState(history[index]);
232
+ scheduleChange();
217
233
  historyIndexRef.current = index;
218
234
  setHistoryIndex(index);
219
235
  setSelectedId(prev => prev && prefabStore.getState().nodesById[prev] ? prev : null);
220
- }, [detachTransformControls, history, prefabStore]);
236
+ }, [detachTransformControls, history, prefabStore, scheduleChange]);
221
237
  const undo = useCallback(() => {
222
238
  if (historyIndex > 0) {
223
239
  applyHistory(historyIndex - 1);
@@ -319,11 +335,11 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
319
335
  function handleDrop(e) {
320
336
  e.preventDefault();
321
337
  e.stopPropagation();
322
- const scene = prefabRootRef.current;
338
+ const runtime = runtimeRef.current;
323
339
  void loadDroppedAssets(e.dataTransfer, {
324
340
  onModelLoaded: (model, filename, file) => {
325
341
  const path = getPrefabAssetRef(filename, 'models');
326
- scene === null || scene === void 0 ? void 0 : scene.addModel(path, model);
342
+ runtime === null || runtime === void 0 ? void 0 : runtime.registerModel(path, model);
327
343
  const modelName = file.name.replace(/\.[^.]+$/, '');
328
344
  const modelIdPrefix = modelName.replace(/[^\w-]+/g, '-') || 'model';
329
345
  if (hasCollisionMeshConventions(model)) {
@@ -336,7 +352,7 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
336
352
  return key;
337
353
  },
338
354
  });
339
- textureRefs.forEach((texture, path) => scene === null || scene === void 0 ? void 0 : scene.addTexture(path, texture));
355
+ textureRefs.forEach((texture, path) => { runtime === null || runtime === void 0 ? void 0 : runtime.registerTexture(path, texture); });
340
356
  add(Object.assign(Object.assign({}, decomposed), { name: modelName || decomposed.name }));
341
357
  return;
342
358
  }
@@ -344,7 +360,7 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
344
360
  },
345
361
  onTextureLoaded: (texture, filename, file) => {
346
362
  const path = getPrefabAssetRef(filename, 'textures');
347
- scene === null || scene === void 0 ? void 0 : scene.addTexture(path, texture);
363
+ runtime === null || runtime === void 0 ? void 0 : runtime.registerTexture(path, texture);
348
364
  add(createImageNode(path, file.name.replace(/\.[^.]+$/, '')));
349
365
  },
350
366
  onLoadError: error => {
@@ -376,44 +392,44 @@ const PrefabEditor = forwardRef(({ basePath = "", initialPrefab, mode: initialMo
376
392
  duplicate,
377
393
  move,
378
394
  replace,
379
- addModel: (path, model) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addModel(path, model); },
380
- addTexture: (path, texture) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addTexture(path, texture); },
381
- addSound: (path, sound) => { var _a; return (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addSound(path, sound); },
395
+ addModel: (path, model) => { var _a; return (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.registerModel(path, model); },
396
+ addTexture: (path, texture) => { var _a; return (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.registerTexture(path, texture); },
397
+ addSound: (path, sound) => { var _a; return (_a = runtimeRef.current) === null || _a === void 0 ? void 0 : _a.registerSound(path, sound); },
382
398
  }), [add, basePath, duplicate, getHandle, getModel, getNode, getObject, getRoot, mode, move, remove, replace, replaceNode, update]);
383
399
  const editorRefValue = useMemo(() => (Object.assign(Object.assign({}, sceneValue), { save: getPrefab, load: loadPrefab, undo,
384
400
  redo, screenshot: handleScreenshot, exportGLB: handleExportGLB, exportGLBData: handleExportGLBData, clearSelection })), [clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, redo, sceneValue, undo]);
385
401
  useImperativeHandle(ref, () => editorRefValue, [editorRefValue]);
386
- const content = (_jsxs(_Fragment, { children: [isEditMode ? _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }) : null, _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath, children: children })] }));
402
+ const content = (_jsxs(_Fragment, { children: [isEditMode ? _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }) : null, _jsx(PrefabRoot, { store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath, children: children })] }));
387
403
  const handleCanvasCreated = useCallback((state) => {
388
404
  var _a;
389
405
  canvasRef.current = state.gl.domElement;
390
406
  (_a = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onCreated) === null || _a === void 0 ? void 0 : _a.call(canvasProps, state);
391
407
  }, [canvasProps]);
392
- return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsx(EditorRefContext.Provider, { value: editorRefValue, children: _jsx(EditorContext.Provider, { value: {
393
- mode,
394
- basePath,
395
- setMode: updateMode,
396
- transformMode,
397
- setTransformMode,
398
- scaleSnap,
399
- setScaleSnap,
400
- positionSnap,
401
- setPositionSnap,
402
- rotationSnap,
403
- setRotationSnap,
404
- onFocusNode: isEditMode ? handleFocusNode : undefined,
405
- onScreenshot: handleScreenshot,
406
- onExportGLB: handleExportGLB
407
- }, children: _jsxs(SceneContext.Provider, { value: sceneValue, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] } }, canvasProps, { onCreated: handleCanvasCreated, onPointerMissed: isEditMode
408
- ? (event) => {
409
- var _a, _b, _c, _d;
410
- const button = (_c = (_a = event.button) !== null && _a !== void 0 ? _a : (_b = event.sourceEvent) === null || _b === void 0 ? void 0 : _b.button) !== null && _c !== void 0 ? _c : 0;
411
- if (button === 0 && selectedId) {
412
- setSelection(null);
408
+ return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsx(AssetRuntimeProvider, { runtimeRef: runtimeRef, children: _jsx(EditorRefContext.Provider, { value: editorRefValue, children: _jsx(EditorContext.Provider, { value: {
409
+ mode,
410
+ basePath,
411
+ setMode: updateMode,
412
+ transformMode,
413
+ setTransformMode,
414
+ scaleSnap,
415
+ setScaleSnap,
416
+ positionSnap,
417
+ setPositionSnap,
418
+ rotationSnap,
419
+ setRotationSnap,
420
+ onFocusNode: isEditMode ? handleFocusNode : undefined,
421
+ onScreenshot: handleScreenshot,
422
+ onExportGLB: handleExportGLB
423
+ }, children: _jsxs(SceneContext.Provider, { value: sceneValue, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] } }, canvasProps, { onCreated: handleCanvasCreated, onPointerMissed: isEditMode
424
+ ? (event) => {
425
+ var _a, _b, _c, _d;
426
+ const button = (_c = (_a = event.button) !== null && _a !== void 0 ? _a : (_b = event.sourceEvent) === null || _b === void 0 ? void 0 : _b.button) !== null && _c !== void 0 ? _c : 0;
427
+ if (button === 0 && selectedId) {
428
+ setSelection(null);
429
+ }
430
+ (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
413
431
  }
414
- (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
415
- }
416
- : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: transformMode === "translate" ? "world" : "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { type: "button", style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) }) }) });
432
+ : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: transformMode === "translate" ? "world" : "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { type: "button", style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) }) }) }) });
417
433
  });
418
434
  PrefabEditor.displayName = "PrefabEditor";
419
435
  export default PrefabEditor;
@@ -1,6 +1,6 @@
1
1
  import { Matrix4 } from "three";
2
2
  import type { Object3D } 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";
@@ -18,7 +18,7 @@ export interface PrefabRootProps {
18
18
  children?: React.ReactNode;
19
19
  }
20
20
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<PrefabRootProps & import("react").RefAttributes<Scene>>;
21
- export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
21
+ export declare function GameObjectRenderer(props: RendererProps): import("react").JSX.Element | null;
22
22
  interface RendererProps {
23
23
  nodeId: string;
24
24
  selectedId?: string | null;