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