Compare commits
2 Commits
b5a9772248
...
63bd5e3715
| Author | SHA1 | Date |
|---|---|---|
|
|
63bd5e3715 | |
|
|
2dd3aff737 |
|
|
@ -1,13 +1,13 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { ObjectInstance, R3, Runtime } from "../types";
|
||||
import { useRef, type RefObject } from "react";
|
||||
import { useMemo, useRef, type RefObject } from "react";
|
||||
import type { Group } from "three";
|
||||
import { TransformControls, useHelper } from "@react-three/drei";
|
||||
import { BoxHelper } from "three";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import { state } from "../state";
|
||||
import { nextSelectionEditMode } from "../state/worldEditorState";
|
||||
import { ObjectViewInternal } from "./ObjectViewInternal";
|
||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
||||
|
||||
type ObjectEditorViewProps = {
|
||||
object: Runtime<ObjectInstance>;
|
||||
|
|
@ -15,17 +15,23 @@ type ObjectEditorViewProps = {
|
|||
|
||||
type SelectionOverlayProps = {
|
||||
objectId: string;
|
||||
groupRef: RefObject<Group | null>;
|
||||
ref: RefObject<ObjectViewInternalHandle | null>;
|
||||
onTransformEnd: () => void;
|
||||
}
|
||||
|
||||
// Separate observer so only the 2 affected instances (selected/deselected)
|
||||
// re-render on selection change, not all N objects in the scene.
|
||||
const SelectionOverlay = observer(function ({ objectId, groupRef, onTransformEnd }: SelectionOverlayProps) {
|
||||
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
|
||||
const isSelected = state.worldEditor.isEnabled &&
|
||||
state.worldEditor.selectedObjectId === objectId;
|
||||
const selectionMode = isSelected ? state.worldEditor.selectedObjectMode : undefined;
|
||||
|
||||
// Stable virtual ref that reads through to the group inside the handle
|
||||
const groupRef = useMemo<RefObject<Group | null>>(
|
||||
() => ({ get current() { return ref.current?.group ?? null; } }),
|
||||
[ref],
|
||||
);
|
||||
|
||||
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
|
||||
|
||||
if (!isSelected || selectionMode === undefined || !groupRef.current)
|
||||
|
|
@ -41,7 +47,7 @@ const SelectionOverlay = observer(function ({ objectId, groupRef, onTransformEnd
|
|||
});
|
||||
|
||||
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
const dataRef = useRef<ObjectViewInternalHandle>(null);
|
||||
|
||||
// Only observes world object types — not selection state — so won't
|
||||
// re-render when a different object is selected.
|
||||
|
|
@ -61,7 +67,7 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
|
|||
}
|
||||
|
||||
function handleTransformEnd() {
|
||||
const group = groupRef.current;
|
||||
const group = dataRef.current?.group;
|
||||
if (group)
|
||||
state.worldEditor.setObjectTransform(
|
||||
object.id,
|
||||
|
|
@ -74,11 +80,11 @@ export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewP
|
|||
return (<>
|
||||
<SelectionOverlay
|
||||
objectId={object.id}
|
||||
groupRef={groupRef}
|
||||
ref={dataRef}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
/>
|
||||
<ObjectViewInternal
|
||||
ref={groupRef}
|
||||
ref={dataRef}
|
||||
object={object}
|
||||
isEditor
|
||||
objectType={objectType}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,83 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import type { ObjectType, RuntimeObjectInstance } from "../types";
|
||||
import { forwardRef } from "react";
|
||||
import type { Group } from "three";
|
||||
import type { ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance } from "../types";
|
||||
import { useEffect, useImperativeHandle, useRef, type Ref } from "react";
|
||||
import { Euler, Quaternion, type Group } from "three";
|
||||
import { Instance, Instances } from "@react-three/drei";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import { ConvexHullCollider } from "@react-three/rapier";
|
||||
import { ConvexHullCollider, RapierRigidBody } from "@react-three/rapier";
|
||||
import { SyncRigidBody } from "./SyncRigidBody";
|
||||
import { reaction, runInAction } from "mobx";
|
||||
import { v3toRapier } from "../utils";
|
||||
|
||||
export type ObjectViewInternalHandle = {
|
||||
group: Group | null;
|
||||
rb: RapierRigidBody | null;
|
||||
}
|
||||
|
||||
type ObjectViewInternalProps = {
|
||||
object: Omit<RuntimeObjectInstance, 'typeId'>;
|
||||
objectType: ObjectType;
|
||||
isEditor?: boolean;
|
||||
onClick?: (e: ThreeEvent<MouseEvent>) => void;
|
||||
ref?: Ref<ObjectViewInternalHandle>;
|
||||
}
|
||||
|
||||
export const ObjectViewInternal = observer(forwardRef<Group | null, ObjectViewInternalProps>(
|
||||
function ({ object, objectType, ...props }, ref) {
|
||||
// not observer, instead reacts only to object.version field
|
||||
export const ObjectViewInternal = function ({ object, objectType, ref, ...props }: ObjectViewInternalProps) {
|
||||
const rbRef = useRef<RapierRigidBody>(null);
|
||||
const groupRef = useRef<Group | null>(null);
|
||||
useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current }));
|
||||
|
||||
useEffect(
|
||||
() => reaction(
|
||||
() => 'version' in object ? object.version : 0,
|
||||
() => {
|
||||
if (!rbRef.current)
|
||||
return;
|
||||
|
||||
const gameObj = object as RuntimeGameObjectInstance;
|
||||
|
||||
// position
|
||||
rbRef.current.setTranslation(v3toRapier(gameObj.position), true);
|
||||
|
||||
// rotation
|
||||
const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]);
|
||||
const q = new Quaternion().setFromEuler(euler);
|
||||
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
|
||||
|
||||
// scale
|
||||
groupRef.current?.scale.set(...gameObj.scale);
|
||||
|
||||
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
|
||||
|
||||
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
|
||||
},
|
||||
),
|
||||
[object.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<SyncRigidBody
|
||||
ref={rbRef}
|
||||
colliders={false}
|
||||
gravityScale={object.physics ? 1 : 0.1}
|
||||
onSync={(_data) => {
|
||||
// TODO rewrite so that is does not trigger re-render
|
||||
// runInAction(() => {
|
||||
// object.position = data.position;
|
||||
// object.rotation = data.rotation;
|
||||
// if (!props.isEditor) {
|
||||
// (object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
|
||||
// (object as RuntimeGameObjectInstance).radialVelocity = data.radialVelocity;
|
||||
// }
|
||||
// });
|
||||
type={object.physics ? 'dynamic' : 'fixed'}
|
||||
gravityScale={object.gravityScale}
|
||||
position={object.position}
|
||||
rotation={object.rotation}
|
||||
onSync={(data) => {
|
||||
runInAction(() => {
|
||||
object.position = data.position;
|
||||
object.rotation = data.rotation;
|
||||
if (!props.isEditor) {
|
||||
(object as RuntimeGameObjectInstance).linearVelocity = data.linearVelocity;
|
||||
(object as RuntimeGameObjectInstance).angularVelocity = data.radialVelocity;
|
||||
}
|
||||
// do not change object.version to not trigger rerender
|
||||
});
|
||||
}}
|
||||
>
|
||||
<group
|
||||
ref={ref}
|
||||
ref={groupRef}
|
||||
name={`${object.id} (${objectType.id} instance)`}
|
||||
position={object.position}
|
||||
rotation={object.rotation}
|
||||
scale={object.scale}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
|
|
@ -54,5 +94,4 @@ export const ObjectViewInternal = observer(forwardRef<Group | null, ObjectViewIn
|
|||
</group>
|
||||
</SyncRigidBody>
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export class GameState {
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// @ts-ignore
|
||||
private withoutAutoSave(fn: () => void) {
|
||||
this._stopAutoSave();
|
||||
fn();
|
||||
|
|
@ -100,7 +102,7 @@ export class GameState {
|
|||
public setCharacterTransform(
|
||||
transform: Pos3,
|
||||
linearVelocity?: V3,
|
||||
radialVelocity?: R3,
|
||||
angularVelocity?: R3,
|
||||
): void {
|
||||
if (this.isPaused)
|
||||
return;
|
||||
|
|
@ -108,8 +110,8 @@ export class GameState {
|
|||
this.scene.character.transform = transform;
|
||||
if (linearVelocity)
|
||||
this.scene.character.linearVelocity = linearVelocity;
|
||||
if (radialVelocity)
|
||||
this.scene.character.radialVelocity = radialVelocity;
|
||||
if (angularVelocity)
|
||||
this.scene.character.angularVelocity = angularVelocity;
|
||||
|
||||
// console.log(`changed character to ${JSON.stringify(this.scene.character)}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ export class WorldState {
|
|||
.map((_, idx) => ({
|
||||
id: `obj${idx}`,
|
||||
typeId: 'wolf',
|
||||
physics: true,
|
||||
gravityScale: 1,
|
||||
position: [idx * 10 - 10, 0, 0],
|
||||
position: [idx * 10 - 10, 5, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
} as ObjectInstance));
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ export type ObjectInstance = {
|
|||
}
|
||||
|
||||
export type GameObjectData = {
|
||||
version: number; // increment to force three and physics changes
|
||||
linearVelocity: V3;
|
||||
radialVelocity: R3;
|
||||
angularVelocity: R3;
|
||||
}
|
||||
|
||||
export type GameObjectInstance = ObjectInstance & GameObjectData;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import type { V3 } from '../types';
|
||||
import { type Vector3Object } from '@react-three/rapier';
|
||||
|
||||
export function clone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
|
|
@ -7,3 +9,11 @@ export function clone<T>(obj: T): T {
|
|||
export function randomId(): string {
|
||||
return uuid();
|
||||
}
|
||||
|
||||
export function v3toRapier(value: V3): Vector3Object {
|
||||
return {
|
||||
x: value[0],
|
||||
y: value[1],
|
||||
z: value[2],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue