react-three-game 0.0.68 → 0.0.70
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/dist/helpers/SoundManager.d.ts +2 -0
- package/dist/helpers/SoundManager.js +6 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -4
- package/dist/shared/GameCanvas.js +0 -2
- package/dist/tools/assetviewer/page.d.ts +5 -0
- package/dist/tools/assetviewer/page.js +3 -0
- package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -2
- package/dist/tools/dragdrop/DragDropLoader.js +18 -3
- package/dist/tools/dragdrop/index.d.ts +2 -2
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -0
- package/dist/tools/dragdrop/modelLoader.js +60 -0
- package/dist/tools/prefabeditor/EditorTree.js +6 -30
- package/dist/tools/prefabeditor/EditorTreeMenus.js +3 -3
- package/dist/tools/prefabeditor/EditorUI.js +6 -4
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +2 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +54 -52
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +22 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +68 -27
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +5 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +148 -145
- package/dist/tools/prefabeditor/components/ClickComponent.js +10 -7
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +10 -4
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +6 -6
- package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
- package/dist/tools/prefabeditor/components/Input.d.ts +16 -0
- package/dist/tools/prefabeditor/components/Input.js +33 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +10 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +35 -43
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -1
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +122 -28
- package/dist/tools/prefabeditor/components/SoundComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/SoundComponent.js +240 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -1
- package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
- package/dist/tools/prefabeditor/prefabStore.js +11 -13
- package/dist/tools/prefabeditor/sceneApi.d.ts +15 -1
- package/dist/tools/prefabeditor/sceneApi.js +77 -32
- package/dist/tools/prefabeditor/styles.d.ts +1 -0
- package/dist/tools/prefabeditor/styles.js +9 -0
- package/dist/tools/prefabeditor/types.d.ts +13 -0
- package/dist/tools/prefabeditor/types.js +28 -1
- package/dist/tools/prefabeditor/useClickValid.d.ts +13 -0
- package/dist/tools/prefabeditor/useClickValid.js +21 -0
- package/dist/tools/prefabeditor/utils.d.ts +2 -0
- package/dist/tools/prefabeditor/utils.js +34 -35
- package/package.json +1 -1
- package/dist/tools/prefabeditor/EditorContext.d.ts +0 -16
- package/dist/tools/prefabeditor/EditorContext.js +0 -9
|
@@ -16,29 +16,31 @@ import { InstancedRigidBodies } from "@react-three/rapier";
|
|
|
16
16
|
import { ActiveCollisionTypes } from "@dimforge/rapier3d-compat";
|
|
17
17
|
import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
|
|
18
18
|
import { gameEvents, getEntityIdFromRigidBody } from "./GameEvents";
|
|
19
|
+
import { useClickValid } from "./useClickValid";
|
|
19
20
|
export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
|
|
20
21
|
export function normalizeRepeatAxes(value) {
|
|
21
22
|
if (!Array.isArray(value)) {
|
|
22
23
|
return DEFAULT_REPEAT_AXES;
|
|
23
24
|
}
|
|
24
25
|
const seen = new Set();
|
|
25
|
-
const normalized = value.
|
|
26
|
+
const normalized = value.reduce((result, entry) => {
|
|
26
27
|
if (!entry || typeof entry !== 'object')
|
|
27
|
-
return
|
|
28
|
+
return result;
|
|
28
29
|
const axisValue = entry.axis;
|
|
29
30
|
if (axisValue !== 'x' && axisValue !== 'y' && axisValue !== 'z')
|
|
30
|
-
return
|
|
31
|
+
return result;
|
|
31
32
|
if (seen.has(axisValue))
|
|
32
|
-
return
|
|
33
|
+
return result;
|
|
33
34
|
seen.add(axisValue);
|
|
34
35
|
const countValue = Number(entry.count);
|
|
35
36
|
const offsetValue = Number(entry.offset);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
result.push({
|
|
38
|
+
axis: axisValue,
|
|
39
|
+
count: Number.isFinite(countValue) ? Math.max(1, Math.floor(countValue)) : 1,
|
|
40
|
+
offset: Number.isFinite(offsetValue) ? offsetValue : 1,
|
|
41
|
+
});
|
|
42
|
+
return result;
|
|
43
|
+
}, []);
|
|
42
44
|
return normalized.length > 0 ? normalized : DEFAULT_REPEAT_AXES;
|
|
43
45
|
}
|
|
44
46
|
function toVector3Tuple(value, fallback) {
|
|
@@ -101,36 +103,17 @@ function hasPhysics(instance) {
|
|
|
101
103
|
function getColliderType(physics) {
|
|
102
104
|
return physics.colliders || (physics.type === 'fixed' ? 'trimesh' : 'hull');
|
|
103
105
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
targetRigidBody: payload.other.rigidBody,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function emitSensorExit(sourceId, payload) {
|
|
112
|
-
gameEvents.emit('sensor:exit', {
|
|
113
|
-
sourceEntityId: sourceId,
|
|
114
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
115
|
-
targetRigidBody: payload.other.rigidBody,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
function emitCollisionEnter(sourceId, payload) {
|
|
119
|
-
gameEvents.emit('collision:enter', {
|
|
120
|
-
sourceEntityId: sourceId,
|
|
121
|
-
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
122
|
-
targetRigidBody: payload.other.rigidBody,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
function emitCollisionExit(sourceId, payload) {
|
|
126
|
-
gameEvents.emit('collision:exit', {
|
|
106
|
+
function emitPhysicsEvent(sourceId, eventName, payload) {
|
|
107
|
+
if (!eventName)
|
|
108
|
+
return;
|
|
109
|
+
gameEvents.emit(eventName, {
|
|
127
110
|
sourceEntityId: sourceId,
|
|
128
111
|
targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
|
|
129
112
|
targetRigidBody: payload.other.rigidBody,
|
|
130
113
|
});
|
|
131
114
|
}
|
|
132
|
-
function emitClick(sourceId, instanceId, event) {
|
|
133
|
-
gameEvents.emit(
|
|
115
|
+
function emitClick(sourceId, instanceId, eventName, event) {
|
|
116
|
+
gameEvents.emit(eventName, {
|
|
134
117
|
sourceEntityId: sourceId,
|
|
135
118
|
instanceEntityId: instanceId && instanceId !== sourceId ? instanceId : undefined,
|
|
136
119
|
point: [event.point.x, event.point.y, event.point.z],
|
|
@@ -145,6 +128,7 @@ function instanceEquals(a, b) {
|
|
|
145
128
|
return a.id === b.id &&
|
|
146
129
|
a.sourceId === b.sourceId &&
|
|
147
130
|
a.clickable === b.clickable &&
|
|
131
|
+
a.clickEventName === b.clickEventName &&
|
|
148
132
|
a.locked === b.locked &&
|
|
149
133
|
a.meshPath === b.meshPath &&
|
|
150
134
|
arrayEquals(a.position, b.position) &&
|
|
@@ -261,7 +245,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
|
|
|
261
245
|
const rigidBodiesRef = useRef(null);
|
|
262
246
|
const instances = useMemo(() => group.instances.filter(hasPhysics).map(inst => {
|
|
263
247
|
const _a = inst.physics, { activeCollisionTypes: _activeCollisionTypes, colliders: _colliders, userData } = _a, rigidBodyProps = __rest(_a, ["activeCollisionTypes", "colliders", "userData"]);
|
|
264
|
-
return Object.assign(Object.assign({ key: inst.id, position: inst.position, rotation: inst.rotation, scale: inst.scale }, rigidBodyProps), { colliders: getColliderType(inst.physics), userData: Object.assign(Object.assign({}, userData), { entityId: inst.sourceId }), onIntersectionEnter: (payload) =>
|
|
248
|
+
return Object.assign(Object.assign({ key: inst.id, position: inst.position, rotation: inst.rotation, scale: inst.scale }, rigidBodyProps), { colliders: getColliderType(inst.physics), userData: Object.assign(Object.assign({}, userData), { entityId: inst.sourceId }), onIntersectionEnter: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.sensorEnterEventName, payload), onIntersectionExit: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.sensorExitEventName, payload), onCollisionEnter: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.collisionEnterEventName, payload), onCollisionExit: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.collisionExitEventName, payload) });
|
|
265
249
|
}), [group.instances]);
|
|
266
250
|
// Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
|
|
267
251
|
useEffect(() => {
|
|
@@ -336,7 +320,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
|
|
|
336
320
|
if (!instance.clickable)
|
|
337
321
|
return;
|
|
338
322
|
e.stopPropagation();
|
|
339
|
-
emitClick(instance.sourceId, instance.id, e);
|
|
323
|
+
emitClick(instance.sourceId, instance.id, instance.clickEventName || 'click', e);
|
|
340
324
|
};
|
|
341
325
|
const shouldHandleClick = editMode || group.instances.some(inst => inst.clickable);
|
|
342
326
|
// Add key to force remount when instance count changes significantly (helps with cleanup)
|
|
@@ -356,12 +340,19 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
|
|
|
356
340
|
}
|
|
357
341
|
// Individual instance item with its own click state
|
|
358
342
|
function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
|
|
359
|
-
const clickValid = useRef(false);
|
|
360
343
|
const groupRef = useRef(null);
|
|
361
344
|
const isLocked = Boolean(instance.locked);
|
|
362
345
|
const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
|
|
363
346
|
const canSelect = editMode && !isLocked;
|
|
364
347
|
const canClick = !editMode && Boolean(instance.clickable);
|
|
348
|
+
const pointerHandlers = useClickValid(canSelect || canClick, (e) => {
|
|
349
|
+
if (editMode) {
|
|
350
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
|
|
351
|
+
}
|
|
352
|
+
else if (instance.clickable) {
|
|
353
|
+
emitClick(instance.sourceId, instance.id, instance.clickEventName || 'click', e);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
365
356
|
// Use BoxHelper when object is selected in edit mode
|
|
366
357
|
useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
|
|
367
358
|
useEffect(() => {
|
|
@@ -370,18 +361,7 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
|
|
|
370
361
|
registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
|
|
371
362
|
return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
|
|
372
363
|
}, [editMode, instance.id, registerRef]);
|
|
373
|
-
return (_jsx("group", { ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale,
|
|
374
|
-
if (clickValid.current) {
|
|
375
|
-
e.stopPropagation();
|
|
376
|
-
if (editMode) {
|
|
377
|
-
onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
|
|
378
|
-
}
|
|
379
|
-
else if (instance.clickable) {
|
|
380
|
-
emitClick(instance.sourceId, instance.id, e);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
clickValid.current = false;
|
|
384
|
-
} : undefined, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
|
|
364
|
+
return (_jsx("group", Object.assign({ ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale }, pointerHandlers, { children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) })));
|
|
385
365
|
}
|
|
386
366
|
// Hook to check if an instance exists
|
|
387
367
|
export function useInstanceCheck(id) {
|
|
@@ -390,21 +370,43 @@ export function useInstanceCheck(id) {
|
|
|
390
370
|
return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
|
|
391
371
|
}
|
|
392
372
|
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
393
|
-
export const GameInstance = React.forwardRef(({ id, sourceId, clickable = false, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
|
|
373
|
+
export const GameInstance = React.forwardRef(({ id, sourceId, clickable = false, clickEventName, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
|
|
394
374
|
const ctx = useContext(GameInstanceContext);
|
|
395
375
|
const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
|
|
396
376
|
const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
|
|
377
|
+
const [positionX, positionY, positionZ] = position;
|
|
378
|
+
const [rotationX, rotationY, rotationZ] = rotation;
|
|
379
|
+
const [scaleX, scaleY, scaleZ] = scale;
|
|
380
|
+
const physicsSignature = getPhysicsSignature(physics);
|
|
397
381
|
const instance = useMemo(() => ({
|
|
398
382
|
id,
|
|
399
383
|
sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
|
|
400
384
|
clickable,
|
|
385
|
+
clickEventName,
|
|
401
386
|
locked,
|
|
402
387
|
meshPath: modelUrl,
|
|
403
388
|
position,
|
|
404
389
|
rotation,
|
|
405
390
|
scale,
|
|
406
391
|
physics,
|
|
407
|
-
}), [
|
|
392
|
+
}), [
|
|
393
|
+
id,
|
|
394
|
+
sourceId,
|
|
395
|
+
clickable,
|
|
396
|
+
clickEventName,
|
|
397
|
+
locked,
|
|
398
|
+
modelUrl,
|
|
399
|
+
positionX,
|
|
400
|
+
positionY,
|
|
401
|
+
positionZ,
|
|
402
|
+
rotationX,
|
|
403
|
+
rotationY,
|
|
404
|
+
rotationZ,
|
|
405
|
+
scaleX,
|
|
406
|
+
scaleY,
|
|
407
|
+
scaleZ,
|
|
408
|
+
physicsSignature,
|
|
409
|
+
]);
|
|
408
410
|
useEffect(() => {
|
|
409
411
|
if (!addInstance || !removeInstance)
|
|
410
412
|
return;
|
|
@@ -19,10 +19,32 @@ export interface PrefabEditorRef {
|
|
|
19
19
|
addTexture: (path: string, texture: Texture, options?: SpawnOptions) => GameObject;
|
|
20
20
|
viewRef: React.RefObject<PrefabRootRef | null>;
|
|
21
21
|
}
|
|
22
|
+
export declare enum PrefabEditorMode {
|
|
23
|
+
Edit = "edit",
|
|
24
|
+
Play = "play"
|
|
25
|
+
}
|
|
26
|
+
export interface EditorContextType {
|
|
27
|
+
mode: PrefabEditorMode;
|
|
28
|
+
setMode: (mode: PrefabEditorMode) => void;
|
|
29
|
+
transformMode: "translate" | "rotate" | "scale";
|
|
30
|
+
setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
|
|
31
|
+
scaleSnap: number;
|
|
32
|
+
setScaleSnap: (resolution: number) => void;
|
|
33
|
+
positionSnap: number;
|
|
34
|
+
setPositionSnap: (resolution: number) => void;
|
|
35
|
+
rotationSnap: number;
|
|
36
|
+
setRotationSnap: (resolution: number) => void;
|
|
37
|
+
onFocusNode?: (nodeId: string) => void;
|
|
38
|
+
onScreenshot?: () => void;
|
|
39
|
+
onExportGLB?: () => void;
|
|
40
|
+
}
|
|
41
|
+
export declare const EditorContext: import("react").Context<EditorContextType | null>;
|
|
42
|
+
export declare function useEditorContext(): EditorContextType;
|
|
22
43
|
export interface PrefabEditorProps {
|
|
23
44
|
basePath?: string;
|
|
24
45
|
initialPrefab?: Prefab;
|
|
25
46
|
physics?: boolean;
|
|
47
|
+
mode?: PrefabEditorMode;
|
|
26
48
|
onChange?: (prefab: Prefab) => void;
|
|
27
49
|
showUI?: boolean;
|
|
28
50
|
enableWindowDrop?: boolean;
|
|
@@ -10,16 +10,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { MapControls, TransformControls } from "@react-three/drei";
|
|
12
12
|
import GameCanvas from "../../shared/GameCanvas";
|
|
13
|
-
import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
|
|
14
|
+
import { findComponentEntry } from "./types";
|
|
14
15
|
import PrefabRoot from "./PrefabRoot";
|
|
15
16
|
import { Physics } from "@react-three/rapier";
|
|
16
17
|
import EditorUI from "./EditorUI";
|
|
17
18
|
import { base, toolbar } from "./styles";
|
|
18
|
-
import { EditorContext } from "./EditorContext";
|
|
19
19
|
import { createImageNode, createModelNode, computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
|
|
20
20
|
import { loadFiles } from "../dragdrop";
|
|
21
21
|
import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab } from "./prefabStore";
|
|
22
22
|
import { createScene } from "./sceneApi";
|
|
23
|
+
export var PrefabEditorMode;
|
|
24
|
+
(function (PrefabEditorMode) {
|
|
25
|
+
PrefabEditorMode["Edit"] = "edit";
|
|
26
|
+
PrefabEditorMode["Play"] = "play";
|
|
27
|
+
})(PrefabEditorMode || (PrefabEditorMode = {}));
|
|
28
|
+
export const EditorContext = createContext(null);
|
|
29
|
+
export function useEditorContext() {
|
|
30
|
+
const context = useContext(EditorContext);
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error("useEditorContext must be used within EditorContext.Provider");
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
const MAX_HISTORY_LENGTH = 50;
|
|
37
|
+
const HISTORY_DEBOUNCE_MS = 500;
|
|
23
38
|
const DEFAULT_PREFAB = {
|
|
24
39
|
id: "prefab-default",
|
|
25
40
|
name: "New Prefab",
|
|
@@ -33,8 +48,8 @@ const DEFAULT_PREFAB = {
|
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
};
|
|
36
|
-
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
37
|
-
const [
|
|
51
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
52
|
+
const [mode, setMode] = useState(initialMode);
|
|
38
53
|
const [selectedId, setSelectedId] = useState(null);
|
|
39
54
|
const [transformMode, setTransformMode] = useState("translate");
|
|
40
55
|
const [scaleSnap, setScaleSnap] = useState(0);
|
|
@@ -53,6 +68,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
53
68
|
const onChangeRef = useRef(onChange);
|
|
54
69
|
const [injectedModels, setInjectedModels] = useState({});
|
|
55
70
|
const [injectedTextures, setInjectedTextures] = useState({});
|
|
71
|
+
const isEditMode = mode === PrefabEditorMode.Edit;
|
|
56
72
|
const getPrefab = useCallback(() => prefabStoreToPrefab(prefabStore.getState()), [prefabStore]);
|
|
57
73
|
onChangeRef.current = onChange;
|
|
58
74
|
const setSelection = useCallback((nodeId) => {
|
|
@@ -62,16 +78,23 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
62
78
|
}
|
|
63
79
|
setSelectedId(nodeId);
|
|
64
80
|
}, [prefabStore]);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const updateMode = useCallback((nextMode) => {
|
|
82
|
+
setMode(prev => {
|
|
83
|
+
if (prev === nextMode)
|
|
84
|
+
return prev;
|
|
85
|
+
if (nextMode === PrefabEditorMode.Play) {
|
|
69
86
|
setSelectedId(null);
|
|
70
87
|
setSelectedObject(null);
|
|
71
88
|
}
|
|
72
|
-
return
|
|
89
|
+
return nextMode;
|
|
73
90
|
});
|
|
91
|
+
}, []);
|
|
92
|
+
const toggleMode = () => {
|
|
93
|
+
updateMode(isEditMode ? PrefabEditorMode.Play : PrefabEditorMode.Edit);
|
|
74
94
|
};
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
updateMode(initialMode);
|
|
97
|
+
}, [initialMode, updateMode]);
|
|
75
98
|
const loadPrefab = useCallback((prefab, options) => {
|
|
76
99
|
changeOriginRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? "replace-silent" : "replace";
|
|
77
100
|
prefabStore.getState().replacePrefab(prefab);
|
|
@@ -110,7 +133,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
110
133
|
clearTimeout(historyTimeout);
|
|
111
134
|
historyTimeout = null;
|
|
112
135
|
}
|
|
113
|
-
if (changeOrigin || !
|
|
136
|
+
if (changeOrigin || !isEditMode) {
|
|
114
137
|
changeOriginRef.current = null;
|
|
115
138
|
return;
|
|
116
139
|
}
|
|
@@ -118,13 +141,13 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
118
141
|
const currentHistoryIndex = historyIndexRef.current;
|
|
119
142
|
setHistory(prev => {
|
|
120
143
|
const nextHistory = [...prev.slice(0, currentHistoryIndex + 1), nextPrefab];
|
|
121
|
-
return nextHistory.length >
|
|
144
|
+
return nextHistory.length > MAX_HISTORY_LENGTH ? nextHistory.slice(1) : nextHistory;
|
|
122
145
|
});
|
|
123
|
-
const nextHistoryIndex = Math.min(currentHistoryIndex + 1,
|
|
146
|
+
const nextHistoryIndex = Math.min(currentHistoryIndex + 1, MAX_HISTORY_LENGTH - 1);
|
|
124
147
|
historyIndexRef.current = nextHistoryIndex;
|
|
125
148
|
setHistoryIndex(nextHistoryIndex);
|
|
126
149
|
historyTimeout = null;
|
|
127
|
-
},
|
|
150
|
+
}, HISTORY_DEBOUNCE_MS);
|
|
128
151
|
});
|
|
129
152
|
return () => {
|
|
130
153
|
if (historyTimeout) {
|
|
@@ -132,7 +155,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
132
155
|
}
|
|
133
156
|
unsubscribe();
|
|
134
157
|
};
|
|
135
|
-
}, [
|
|
158
|
+
}, [isEditMode, prefabStore]);
|
|
136
159
|
useEffect(() => {
|
|
137
160
|
if (!selectedId)
|
|
138
161
|
return;
|
|
@@ -187,7 +210,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
187
210
|
const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
|
|
188
211
|
const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
|
|
189
212
|
useEffect(() => {
|
|
190
|
-
if (!
|
|
213
|
+
if (!isEditMode)
|
|
191
214
|
return;
|
|
192
215
|
const handleKeyDown = (e) => {
|
|
193
216
|
if (!(e.ctrlKey || e.metaKey))
|
|
@@ -203,7 +226,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
203
226
|
};
|
|
204
227
|
window.addEventListener('keydown', handleKeyDown);
|
|
205
228
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
206
|
-
}, [
|
|
229
|
+
}, [isEditMode, historyIndex, history]);
|
|
207
230
|
const handleScreenshot = useCallback(() => {
|
|
208
231
|
const canvas = canvasRef.current;
|
|
209
232
|
if (!canvas)
|
|
@@ -253,6 +276,18 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
253
276
|
const scene = useMemo(() => createScene({
|
|
254
277
|
getRootId: () => prefabStore.getState().rootId,
|
|
255
278
|
getNode: (id) => { var _a; return (_a = prefabStore.getState().nodesById[id]) !== null && _a !== void 0 ? _a : null; },
|
|
279
|
+
getChildIds: (id) => { var _a; return (_a = prefabStore.getState().childIdsById[id]) !== null && _a !== void 0 ? _a : []; },
|
|
280
|
+
getParentId: (id) => { var _a; return (_a = prefabStore.getState().parentIdById[id]) !== null && _a !== void 0 ? _a : null; },
|
|
281
|
+
findByName: (name) => {
|
|
282
|
+
var _a;
|
|
283
|
+
const state = prefabStore.getState();
|
|
284
|
+
const normalized = name.toLowerCase();
|
|
285
|
+
for (const [id, node] of Object.entries(state.nodesById)) {
|
|
286
|
+
if (((_a = node.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === normalized)
|
|
287
|
+
return id;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
},
|
|
256
291
|
updateNode: (id, update) => prefabStore.getState().updateNode(id, update),
|
|
257
292
|
updateNodes: (updates) => prefabStore.getState().updateNodes(Object.entries(updates).map(([id, update]) => ({ id, update }))),
|
|
258
293
|
addNode: (node, options) => addNode(node, options).id,
|
|
@@ -268,14 +303,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
268
303
|
const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
|
|
269
304
|
const local = parentWorld.clone().invert().multiply(object.matrixWorld);
|
|
270
305
|
const { position, rotation, scale } = decompose(local);
|
|
271
|
-
prefabStore.getState().updateNode(selectedId, node =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
306
|
+
prefabStore.getState().updateNode(selectedId, node => {
|
|
307
|
+
var _a;
|
|
308
|
+
const entry = findComponentEntry(node, "Transform");
|
|
309
|
+
const key = (_a = entry === null || entry === void 0 ? void 0 : entry[0]) !== null && _a !== void 0 ? _a : "transform";
|
|
310
|
+
return Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { [key]: {
|
|
311
|
+
type: "Transform",
|
|
312
|
+
properties: { position, rotation, scale },
|
|
313
|
+
} }) });
|
|
314
|
+
});
|
|
275
315
|
};
|
|
276
316
|
// --- Drag & drop files to add nodes ---
|
|
277
317
|
useEffect(() => {
|
|
278
|
-
if (!enableWindowDrop || !
|
|
318
|
+
if (!enableWindowDrop || !isEditMode)
|
|
279
319
|
return;
|
|
280
320
|
function handleDragOver(e) {
|
|
281
321
|
e.preventDefault();
|
|
@@ -308,7 +348,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
308
348
|
window.removeEventListener('dragover', handleDragOver);
|
|
309
349
|
window.removeEventListener('drop', handleDrop);
|
|
310
350
|
};
|
|
311
|
-
}, [addModel, addTexture,
|
|
351
|
+
}, [addModel, addTexture, isEditMode, enableWindowDrop]);
|
|
312
352
|
useImperativeHandle(ref, () => ({
|
|
313
353
|
screenshot: handleScreenshot,
|
|
314
354
|
exportGLB: handleExportGLB,
|
|
@@ -321,9 +361,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
321
361
|
addTexture,
|
|
322
362
|
viewRef: prefabRootRef
|
|
323
363
|
}), [addModel, addTexture, clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, scene]);
|
|
324
|
-
const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode:
|
|
364
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, onSelectedObjectChange: isEditMode ? setSelectedObject : undefined, onFocusNode: isEditMode ? handleFocusNode : undefined, basePath: basePath, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
|
|
325
365
|
return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsxs(EditorContext.Provider, { value: {
|
|
326
|
-
|
|
366
|
+
mode,
|
|
367
|
+
setMode: updateMode,
|
|
327
368
|
transformMode,
|
|
328
369
|
setTransformMode,
|
|
329
370
|
scaleSnap,
|
|
@@ -332,10 +373,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
332
373
|
setPositionSnap,
|
|
333
374
|
rotationSnap,
|
|
334
375
|
setRotationSnap,
|
|
335
|
-
onFocusNode:
|
|
376
|
+
onFocusNode: isEditMode ? handleFocusNode : undefined,
|
|
336
377
|
onScreenshot: handleScreenshot,
|
|
337
378
|
onExportGLB: handleExportGLB
|
|
338
|
-
}, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed:
|
|
379
|
+
}, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed: isEditMode
|
|
339
380
|
? (event) => {
|
|
340
381
|
var _a, _b, _c, _d;
|
|
341
382
|
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;
|
|
@@ -344,7 +385,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
344
385
|
}
|
|
345
386
|
(_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
|
|
346
387
|
}
|
|
347
|
-
: canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { debug:
|
|
388
|
+
: canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { debug: isEditMode, paused: isEditMode, children: content })) : content, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("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 }))] }))] }) });
|
|
348
389
|
});
|
|
349
390
|
PrefabEditor.displayName = "PrefabEditor";
|
|
350
391
|
export default PrefabEditor;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Group, Matrix4, Object3D } from "three";
|
|
2
2
|
import { ThreeEvent } from "@react-three/fiber";
|
|
3
3
|
import { GameObject as GameObjectType, Prefab } from "./types";
|
|
4
|
-
import { LoadedModels, LoadedTextures } from "../dragdrop";
|
|
4
|
+
import { LoadedModels, LoadedSounds, LoadedTextures } from "../dragdrop";
|
|
5
5
|
import { PrefabStoreApi } from "./prefabStore";
|
|
6
6
|
export interface PrefabRootRef {
|
|
7
7
|
root: Group | null;
|
|
8
8
|
rigidBodyRefs: Map<string, any>;
|
|
9
9
|
getObject: (nodeId: string) => Object3D | null;
|
|
10
|
+
getRigidBody: (nodeId: string) => any;
|
|
10
11
|
focusNode: (nodeId: string) => void;
|
|
11
12
|
}
|
|
12
13
|
export interface PrefabRootProps {
|
|
@@ -21,6 +22,7 @@ export interface PrefabRootProps {
|
|
|
21
22
|
basePath?: string;
|
|
22
23
|
injectedModels?: LoadedModels;
|
|
23
24
|
injectedTextures?: LoadedTextures;
|
|
25
|
+
injectedSounds?: LoadedSounds;
|
|
24
26
|
}
|
|
25
27
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<PrefabRootProps & import("react").RefAttributes<PrefabRootRef>>;
|
|
26
28
|
export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -31,7 +33,9 @@ interface RendererProps {
|
|
|
31
33
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
32
34
|
registerRef: (id: string, obj: Object3D | null) => void;
|
|
33
35
|
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
36
|
+
getRigidBody: (id: string) => any;
|
|
34
37
|
loadedModels: LoadedModels;
|
|
38
|
+
loadedSounds: LoadedSounds;
|
|
35
39
|
loadedTextures: LoadedTextures;
|
|
36
40
|
editMode?: boolean;
|
|
37
41
|
parentMatrix?: Matrix4;
|