react-three-game 0.0.69 → 0.0.71
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 +20 -13
- package/dist/index.js +14 -7
- 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 -40
- package/dist/tools/prefabeditor/EditorTreeMenus.js +2 -20
- package/dist/tools/prefabeditor/EditorUI.js +8 -5
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +2 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +54 -52
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +23 -1
- package/dist/tools/prefabeditor/PrefabEditor.js +79 -47
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +26 -9
- package/dist/tools/prefabeditor/PrefabRoot.js +195 -159
- package/dist/tools/prefabeditor/RefBridge.d.ts +24 -0
- package/dist/tools/prefabeditor/RefBridge.js +44 -0
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +10 -7
- package/dist/tools/prefabeditor/components/CameraComponent.js +8 -14
- package/dist/tools/prefabeditor/components/ClickComponent.js +12 -7
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +31 -5
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +6 -6
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +124 -52
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +5 -3
- 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 +19 -8
- package/dist/tools/prefabeditor/components/ModelComponent.js +39 -45
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -1
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +127 -31
- package/dist/tools/prefabeditor/components/PointLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/PointLightComponent.js +55 -0
- package/dist/tools/prefabeditor/components/SoundComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/SoundComponent.js +244 -0
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +53 -24
- package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
- package/dist/tools/prefabeditor/components/index.js +4 -0
- package/dist/tools/prefabeditor/components/lightUtils.d.ts +13 -0
- package/dist/tools/prefabeditor/components/lightUtils.js +64 -0
- package/dist/tools/prefabeditor/prefab.d.ts +37 -0
- package/dist/tools/prefabeditor/prefab.js +229 -0
- package/dist/tools/prefabeditor/prefabStore.d.ts +4 -16
- package/dist/tools/prefabeditor/prefabStore.js +32 -173
- package/dist/tools/prefabeditor/{sceneApi.d.ts → scene.d.ts} +15 -1
- package/dist/tools/prefabeditor/{sceneApi.js → scene.js} +66 -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 -4
- package/dist/tools/prefabeditor/utils.js +8 -46
- 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;
|
|
@@ -3,7 +3,7 @@ import { Object3D, Texture } from "three";
|
|
|
3
3
|
import { GameObject, Prefab } from "./types";
|
|
4
4
|
import { PrefabRootRef } from "./PrefabRoot";
|
|
5
5
|
import type { ExportGLBOptions } from "./utils";
|
|
6
|
-
import { type Scene, type SpawnOptions } from "./
|
|
6
|
+
import { type Scene, type SpawnOptions } from "./scene";
|
|
7
7
|
export interface PrefabEditorRef {
|
|
8
8
|
screenshot: () => void;
|
|
9
9
|
exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | undefined>;
|
|
@@ -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,31 +10,39 @@ 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 {
|
|
19
|
-
import { createImageNode, createModelNode, computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
|
|
19
|
+
import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
|
|
20
20
|
import { loadFiles } from "../dragdrop";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { denormalizePrefab, createImageNode, createModelNode, createNode } from './prefab';
|
|
22
|
+
import { createPrefabStore, PrefabStoreProvider } from "./prefabStore";
|
|
23
|
+
import { createScene } from "./scene";
|
|
24
|
+
export var PrefabEditorMode;
|
|
25
|
+
(function (PrefabEditorMode) {
|
|
26
|
+
PrefabEditorMode["Edit"] = "edit";
|
|
27
|
+
PrefabEditorMode["Play"] = "play";
|
|
28
|
+
})(PrefabEditorMode || (PrefabEditorMode = {}));
|
|
29
|
+
export const EditorContext = createContext(null);
|
|
30
|
+
export function useEditorContext() {
|
|
31
|
+
const context = useContext(EditorContext);
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("useEditorContext must be used within EditorContext.Provider");
|
|
34
|
+
}
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
const MAX_HISTORY_LENGTH = 50;
|
|
38
|
+
const HISTORY_DEBOUNCE_MS = 500;
|
|
23
39
|
const DEFAULT_PREFAB = {
|
|
24
40
|
id: "prefab-default",
|
|
25
41
|
name: "New Prefab",
|
|
26
|
-
root: {
|
|
27
|
-
id: "root",
|
|
28
|
-
components: {
|
|
29
|
-
transform: {
|
|
30
|
-
type: "Transform",
|
|
31
|
-
properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
42
|
+
root: createNode('Root', {}, { id: 'root' })
|
|
35
43
|
};
|
|
36
|
-
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
37
|
-
const [
|
|
44
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
45
|
+
const [mode, setMode] = useState(initialMode);
|
|
38
46
|
const [selectedId, setSelectedId] = useState(null);
|
|
39
47
|
const [transformMode, setTransformMode] = useState("translate");
|
|
40
48
|
const [scaleSnap, setScaleSnap] = useState(0);
|
|
@@ -51,9 +59,8 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
51
59
|
const canvasRef = useRef(null);
|
|
52
60
|
const controlsRef = useRef(null);
|
|
53
61
|
const onChangeRef = useRef(onChange);
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const getPrefab = useCallback(() => prefabStoreToPrefab(prefabStore.getState()), [prefabStore]);
|
|
62
|
+
const isEditMode = mode === PrefabEditorMode.Edit;
|
|
63
|
+
const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
|
|
57
64
|
onChangeRef.current = onChange;
|
|
58
65
|
const setSelection = useCallback((nodeId) => {
|
|
59
66
|
const nextNode = nodeId ? prefabStore.getState().nodesById[nodeId] : null;
|
|
@@ -62,24 +69,29 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
62
69
|
}
|
|
63
70
|
setSelectedId(nodeId);
|
|
64
71
|
}, [prefabStore]);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
const updateMode = useCallback((nextMode) => {
|
|
73
|
+
setMode(prev => {
|
|
74
|
+
if (prev === nextMode)
|
|
75
|
+
return prev;
|
|
76
|
+
if (nextMode === PrefabEditorMode.Play) {
|
|
69
77
|
setSelectedId(null);
|
|
70
78
|
setSelectedObject(null);
|
|
71
79
|
}
|
|
72
|
-
return
|
|
80
|
+
return nextMode;
|
|
73
81
|
});
|
|
82
|
+
}, []);
|
|
83
|
+
const toggleMode = () => {
|
|
84
|
+
updateMode(isEditMode ? PrefabEditorMode.Play : PrefabEditorMode.Edit);
|
|
74
85
|
};
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
updateMode(initialMode);
|
|
88
|
+
}, [initialMode, updateMode]);
|
|
75
89
|
const loadPrefab = useCallback((prefab, options) => {
|
|
76
90
|
changeOriginRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? "replace-silent" : "replace";
|
|
77
91
|
prefabStore.getState().replacePrefab(prefab);
|
|
78
92
|
setSelectedObject(null);
|
|
79
93
|
if (options === null || options === void 0 ? void 0 : options.resetHistory) {
|
|
80
94
|
setSelectedId(null);
|
|
81
|
-
setInjectedModels({});
|
|
82
|
-
setInjectedTextures({});
|
|
83
95
|
setHistory([prefab]);
|
|
84
96
|
historyIndexRef.current = 0;
|
|
85
97
|
setHistoryIndex(0);
|
|
@@ -101,7 +113,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
101
113
|
return;
|
|
102
114
|
}
|
|
103
115
|
lastRevision = state.revision;
|
|
104
|
-
const nextPrefab =
|
|
116
|
+
const nextPrefab = denormalizePrefab(state);
|
|
105
117
|
const changeOrigin = changeOriginRef.current;
|
|
106
118
|
if (changeOrigin !== "replace-silent") {
|
|
107
119
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextPrefab);
|
|
@@ -110,7 +122,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
110
122
|
clearTimeout(historyTimeout);
|
|
111
123
|
historyTimeout = null;
|
|
112
124
|
}
|
|
113
|
-
if (changeOrigin || !
|
|
125
|
+
if (changeOrigin || !isEditMode) {
|
|
114
126
|
changeOriginRef.current = null;
|
|
115
127
|
return;
|
|
116
128
|
}
|
|
@@ -118,13 +130,13 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
118
130
|
const currentHistoryIndex = historyIndexRef.current;
|
|
119
131
|
setHistory(prev => {
|
|
120
132
|
const nextHistory = [...prev.slice(0, currentHistoryIndex + 1), nextPrefab];
|
|
121
|
-
return nextHistory.length >
|
|
133
|
+
return nextHistory.length > MAX_HISTORY_LENGTH ? nextHistory.slice(1) : nextHistory;
|
|
122
134
|
});
|
|
123
|
-
const nextHistoryIndex = Math.min(currentHistoryIndex + 1,
|
|
135
|
+
const nextHistoryIndex = Math.min(currentHistoryIndex + 1, MAX_HISTORY_LENGTH - 1);
|
|
124
136
|
historyIndexRef.current = nextHistoryIndex;
|
|
125
137
|
setHistoryIndex(nextHistoryIndex);
|
|
126
138
|
historyTimeout = null;
|
|
127
|
-
},
|
|
139
|
+
}, HISTORY_DEBOUNCE_MS);
|
|
128
140
|
});
|
|
129
141
|
return () => {
|
|
130
142
|
if (historyTimeout) {
|
|
@@ -132,7 +144,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
132
144
|
}
|
|
133
145
|
unsubscribe();
|
|
134
146
|
};
|
|
135
|
-
}, [
|
|
147
|
+
}, [isEditMode, prefabStore]);
|
|
136
148
|
useEffect(() => {
|
|
137
149
|
if (!selectedId)
|
|
138
150
|
return;
|
|
@@ -165,15 +177,17 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
165
177
|
addNode(regenerateIds(prefab.root), { select: false });
|
|
166
178
|
}, [addNode]);
|
|
167
179
|
const addModel = useCallback((path, model, options) => {
|
|
180
|
+
var _a;
|
|
168
181
|
const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
169
182
|
addNode(node, options);
|
|
170
|
-
|
|
183
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addModel(path, model);
|
|
171
184
|
return node;
|
|
172
185
|
}, [addNode]);
|
|
173
186
|
const addTexture = useCallback((path, texture, options) => {
|
|
187
|
+
var _a;
|
|
174
188
|
const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
|
|
175
189
|
addNode(node, options);
|
|
176
|
-
|
|
190
|
+
(_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addTexture(path, texture);
|
|
177
191
|
return node;
|
|
178
192
|
}, [addNode]);
|
|
179
193
|
const applyHistory = (index) => {
|
|
@@ -187,7 +201,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
187
201
|
const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
|
|
188
202
|
const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
|
|
189
203
|
useEffect(() => {
|
|
190
|
-
if (!
|
|
204
|
+
if (!isEditMode)
|
|
191
205
|
return;
|
|
192
206
|
const handleKeyDown = (e) => {
|
|
193
207
|
if (!(e.ctrlKey || e.metaKey))
|
|
@@ -203,7 +217,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
203
217
|
};
|
|
204
218
|
window.addEventListener('keydown', handleKeyDown);
|
|
205
219
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
206
|
-
}, [
|
|
220
|
+
}, [isEditMode, historyIndex, history]);
|
|
207
221
|
const handleScreenshot = useCallback(() => {
|
|
208
222
|
const canvas = canvasRef.current;
|
|
209
223
|
if (!canvas)
|
|
@@ -253,6 +267,18 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
253
267
|
const scene = useMemo(() => createScene({
|
|
254
268
|
getRootId: () => prefabStore.getState().rootId,
|
|
255
269
|
getNode: (id) => { var _a; return (_a = prefabStore.getState().nodesById[id]) !== null && _a !== void 0 ? _a : null; },
|
|
270
|
+
getChildIds: (id) => { var _a; return (_a = prefabStore.getState().childIdsById[id]) !== null && _a !== void 0 ? _a : []; },
|
|
271
|
+
getParentId: (id) => { var _a; return (_a = prefabStore.getState().parentIdById[id]) !== null && _a !== void 0 ? _a : null; },
|
|
272
|
+
findByName: (name) => {
|
|
273
|
+
var _a;
|
|
274
|
+
const state = prefabStore.getState();
|
|
275
|
+
const normalized = name.toLowerCase();
|
|
276
|
+
for (const [id, node] of Object.entries(state.nodesById)) {
|
|
277
|
+
if (((_a = node.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === normalized)
|
|
278
|
+
return id;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
},
|
|
256
282
|
updateNode: (id, update) => prefabStore.getState().updateNode(id, update),
|
|
257
283
|
updateNodes: (updates) => prefabStore.getState().updateNodes(Object.entries(updates).map(([id, update]) => ({ id, update }))),
|
|
258
284
|
addNode: (node, options) => addNode(node, options).id,
|
|
@@ -268,14 +294,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
268
294
|
const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
|
|
269
295
|
const local = parentWorld.clone().invert().multiply(object.matrixWorld);
|
|
270
296
|
const { position, rotation, scale } = decompose(local);
|
|
271
|
-
prefabStore.getState().updateNode(selectedId, node =>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
297
|
+
prefabStore.getState().updateNode(selectedId, node => {
|
|
298
|
+
var _a;
|
|
299
|
+
const entry = findComponentEntry(node, "Transform");
|
|
300
|
+
const key = (_a = entry === null || entry === void 0 ? void 0 : entry[0]) !== null && _a !== void 0 ? _a : "transform";
|
|
301
|
+
return Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { [key]: {
|
|
302
|
+
type: "Transform",
|
|
303
|
+
properties: { position, rotation, scale },
|
|
304
|
+
} }) });
|
|
305
|
+
});
|
|
275
306
|
};
|
|
276
307
|
// --- Drag & drop files to add nodes ---
|
|
277
308
|
useEffect(() => {
|
|
278
|
-
if (!enableWindowDrop || !
|
|
309
|
+
if (!enableWindowDrop || !isEditMode)
|
|
279
310
|
return;
|
|
280
311
|
function handleDragOver(e) {
|
|
281
312
|
e.preventDefault();
|
|
@@ -308,7 +339,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
308
339
|
window.removeEventListener('dragover', handleDragOver);
|
|
309
340
|
window.removeEventListener('drop', handleDrop);
|
|
310
341
|
};
|
|
311
|
-
}, [addModel, addTexture,
|
|
342
|
+
}, [addModel, addTexture, isEditMode, enableWindowDrop]);
|
|
312
343
|
useImperativeHandle(ref, () => ({
|
|
313
344
|
screenshot: handleScreenshot,
|
|
314
345
|
exportGLB: handleExportGLB,
|
|
@@ -321,9 +352,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
321
352
|
addTexture,
|
|
322
353
|
viewRef: prefabRootRef
|
|
323
354
|
}), [addModel, addTexture, clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, scene]);
|
|
324
|
-
const content = (_jsxs(_Fragment, { children: [_jsx("
|
|
355
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath }), children] }));
|
|
325
356
|
return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsxs(EditorContext.Provider, { value: {
|
|
326
|
-
|
|
357
|
+
mode,
|
|
358
|
+
setMode: updateMode,
|
|
327
359
|
transformMode,
|
|
328
360
|
setTransformMode,
|
|
329
361
|
scaleSnap,
|
|
@@ -332,10 +364,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
332
364
|
setPositionSnap,
|
|
333
365
|
rotationSnap,
|
|
334
366
|
setRotationSnap,
|
|
335
|
-
onFocusNode:
|
|
367
|
+
onFocusNode: isEditMode ? handleFocusNode : undefined,
|
|
336
368
|
onScreenshot: handleScreenshot,
|
|
337
369
|
onExportGLB: handleExportGLB
|
|
338
|
-
}, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed:
|
|
370
|
+
}, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed: isEditMode
|
|
339
371
|
? (event) => {
|
|
340
372
|
var _a, _b, _c, _d;
|
|
341
373
|
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 +376,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
|
|
|
344
376
|
}
|
|
345
377
|
(_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
|
|
346
378
|
}
|
|
347
|
-
: canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { debug:
|
|
379
|
+
: 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
380
|
});
|
|
349
381
|
PrefabEditor.displayName = "PrefabEditor";
|
|
350
382
|
export default PrefabEditor;
|
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import { Group, Matrix4, Object3D } from "three";
|
|
1
|
+
import { Group, Matrix4, Object3D, Texture } from "three";
|
|
2
2
|
import { ThreeEvent } from "@react-three/fiber";
|
|
3
3
|
import { GameObject as GameObjectType, Prefab } from "./types";
|
|
4
|
-
import { LoadedModels
|
|
4
|
+
import { LoadedModels } from "../dragdrop";
|
|
5
5
|
import { PrefabStoreApi } from "./prefabStore";
|
|
6
|
+
import { type RefBridge } from "./RefBridge";
|
|
7
|
+
/** Runtime context available to all component Views inside a PrefabRoot. */
|
|
8
|
+
export interface SceneRuntime {
|
|
9
|
+
refBridge: RefBridge;
|
|
10
|
+
getRigidBody: (id: string) => any;
|
|
11
|
+
/** @internal Used by PhysicsComponent. */
|
|
12
|
+
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
13
|
+
editMode?: boolean;
|
|
14
|
+
/** Get a loaded model by asset path. */
|
|
15
|
+
getModel: (path: string) => Object3D | null;
|
|
16
|
+
/** Get a loaded texture by asset path. */
|
|
17
|
+
getTexture: (path: string) => Texture | null;
|
|
18
|
+
/** Get a loaded sound buffer by asset path. */
|
|
19
|
+
getSound: (path: string) => AudioBuffer | null;
|
|
20
|
+
/** Get a revision string that changes when loaded assets change (for cache-busting keys). */
|
|
21
|
+
getAssetRevision: () => string;
|
|
22
|
+
}
|
|
23
|
+
/** Access the scene runtime (refBridge, getRigidBody, editMode) from within a component View. */
|
|
24
|
+
export declare function useSceneRuntime(): SceneRuntime;
|
|
6
25
|
export interface PrefabRootRef {
|
|
7
26
|
root: Group | null;
|
|
27
|
+
refBridge: RefBridge;
|
|
8
28
|
rigidBodyRefs: Map<string, any>;
|
|
9
29
|
getObject: (nodeId: string) => Object3D | null;
|
|
10
|
-
|
|
30
|
+
getRigidBody: (nodeId: string) => any;
|
|
31
|
+
addModel: (path: string, model: Object3D) => void;
|
|
32
|
+
addTexture: (path: string, texture: Texture) => void;
|
|
33
|
+
addSound: (path: string, sound: AudioBuffer) => void;
|
|
11
34
|
}
|
|
12
35
|
export interface PrefabRootProps {
|
|
13
36
|
editMode?: boolean;
|
|
@@ -16,11 +39,7 @@ export interface PrefabRootProps {
|
|
|
16
39
|
selectedId?: string | null;
|
|
17
40
|
onSelect?: (id: string | null) => void;
|
|
18
41
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
19
|
-
onSelectedObjectChange?: (object: Object3D | null) => void;
|
|
20
|
-
onFocusNode?: (nodeId: string) => void;
|
|
21
42
|
basePath?: string;
|
|
22
|
-
injectedModels?: LoadedModels;
|
|
23
|
-
injectedTextures?: LoadedTextures;
|
|
24
43
|
}
|
|
25
44
|
export declare const PrefabRoot: import("react").ForwardRefExoticComponent<PrefabRootProps & import("react").RefAttributes<PrefabRootRef>>;
|
|
26
45
|
export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -30,9 +49,7 @@ interface RendererProps {
|
|
|
30
49
|
onSelect?: (id: string) => void;
|
|
31
50
|
onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
|
|
32
51
|
registerRef: (id: string, obj: Object3D | null) => void;
|
|
33
|
-
registerRigidBodyRef: (id: string, rb: any) => void;
|
|
34
52
|
loadedModels: LoadedModels;
|
|
35
|
-
loadedTextures: LoadedTextures;
|
|
36
53
|
editMode?: boolean;
|
|
37
54
|
parentMatrix?: Matrix4;
|
|
38
55
|
}
|