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.
- package/README.md +4 -11
- package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +46 -8
- package/dist/plugins/crashcat/CrashcatRagdoll.d.ts +1 -1
- package/dist/plugins/crashcat/CrashcatRuntime.d.ts +1 -1
- package/dist/shared/ContactShadow.d.ts +1 -1
- package/dist/shared/GameCanvas.d.ts +1 -1
- package/dist/tools/assetviewer/page.d.ts +10 -10
- package/dist/tools/dragdrop/DragDropLoader.d.ts +2 -2
- package/dist/tools/prefabeditor/Dropdown.d.ts +1 -1
- package/dist/tools/prefabeditor/EditorTree.d.ts +1 -1
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +2 -2
- package/dist/tools/prefabeditor/EditorUI.d.ts +1 -1
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.js +67 -51
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +2 -2
- package/dist/tools/prefabeditor/PrefabRoot.js +73 -208
- package/dist/tools/prefabeditor/SceneProvider.d.ts +14 -0
- package/dist/tools/prefabeditor/SceneProvider.js +68 -0
- package/dist/tools/prefabeditor/assetRuntime.d.ts +25 -2
- package/dist/tools/prefabeditor/assetRuntime.js +115 -1
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +3 -3
- package/dist/tools/prefabeditor/components/Input.d.ts +19 -19
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -1
- package/dist/tools/prefabeditor/components/MaterialComponent.js +13 -14
- package/dist/tools/prefabeditor/components/ModelComponent.js +2 -3
- package/dist/tools/prefabeditor/components/PrefabRefComponent.js +39 -15
- package/dist/tools/prefabeditor/components/SoundComponent.js +6 -2
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -5
- package/dist/tools/prefabeditor/components/lightUtils.d.ts +2 -2
- package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
- package/dist/tools/prefabeditor/prefabStore.js +11 -0
- package/dist/viewer.d.ts +11 -12
- package/dist/viewer.js +7 -9
- package/package.json +9 -9
- package/dist/tools/prefabeditor/components/runtime.d.ts +0 -4
- 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`, `
|
|
284
|
-
* `
|
|
285
|
-
* `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
311
|
-
void
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
38
|
export declare function SingleTextureViewer({ file, basePath }: {
|
|
39
39
|
file?: string;
|
|
40
40
|
basePath?: string;
|
|
41
|
-
}): import("react
|
|
41
|
+
}): import("react").JSX.Element;
|
|
42
42
|
export declare function SingleModelViewer({ file, basePath }: {
|
|
43
43
|
file?: string;
|
|
44
44
|
basePath?: string;
|
|
45
|
-
}): import("react
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
46
|
export declare function SingleSoundViewer({ file, basePath }: {
|
|
47
47
|
file?: string;
|
|
48
48
|
basePath?: string;
|
|
49
|
-
}): import("react
|
|
50
|
-
export declare function SharedCanvas(): import("react
|
|
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
|
|
25
|
-
export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react
|
|
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 {};
|
|
@@ -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
|
|
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
|
|
29
|
+
}): import("react").JSX.Element;
|
|
@@ -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
|
|
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([
|
|
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
|
|
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 =
|
|
85
|
-
const getObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a =
|
|
86
|
-
const getHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a =
|
|
87
|
-
const getModel = useCallback((path) => { var _a, _b; return (_b = (_a =
|
|
88
|
-
|
|
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),
|
|
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
|
-
|
|
113
|
-
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, prefab);
|
|
127
|
+
scheduleChange();
|
|
114
128
|
if (pushHistory)
|
|
115
|
-
scheduleHistory(
|
|
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([
|
|
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().
|
|
216
|
-
(
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
380
|
-
addTexture: (path, texture) => { var _a; return (_a =
|
|
381
|
-
addSound: (path, sound) => { var _a; return (_a =
|
|
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, {
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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;
|