95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { observer } from "mobx-react-lite";
|
|
import type { ObjectInstance, R3, Runtime } from "../types";
|
|
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, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
|
|
|
type ObjectEditorViewProps = {
|
|
object: Runtime<ObjectInstance>;
|
|
}
|
|
|
|
type SelectionOverlayProps = {
|
|
objectId: string;
|
|
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, 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)
|
|
return null;
|
|
|
|
return (
|
|
<TransformControls
|
|
object={groupRef as RefObject<Group>}
|
|
mode={selectionMode}
|
|
onMouseUp={onTransformEnd}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
|
|
const dataRef = useRef<ObjectViewInternalHandle>(null);
|
|
|
|
// Only observes world object types — not selection state — so won't
|
|
// re-render when a different object is selected.
|
|
const objectType = state.world.getObjectTypeById(object.typeId);
|
|
if (!objectType)
|
|
return null;
|
|
|
|
function handleClick(e: ThreeEvent<MouseEvent>) {
|
|
if (e.delta > 5) return;
|
|
e.stopPropagation();
|
|
// Reading selection state inside an event handler: not tracked by observer.
|
|
const currentMode = state.worldEditor.isEnabled &&
|
|
state.worldEditor.selectedObjectId === object.id
|
|
? state.worldEditor.selectedObjectMode
|
|
: undefined;
|
|
state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(currentMode));
|
|
}
|
|
|
|
function handleTransformEnd() {
|
|
const group = dataRef.current?.group;
|
|
if (group)
|
|
state.worldEditor.setObjectTransform(
|
|
object.id,
|
|
group.position.toArray(),
|
|
group.rotation.toArray().slice(0, 3) as R3,
|
|
group.scale.toArray(),
|
|
);
|
|
}
|
|
|
|
return (<>
|
|
<SelectionOverlay
|
|
objectId={object.id}
|
|
ref={dataRef}
|
|
onTransformEnd={handleTransformEnd}
|
|
/>
|
|
<ObjectViewInternal
|
|
ref={dataRef}
|
|
object={object}
|
|
isEditor
|
|
objectType={objectType}
|
|
onClick={handleClick}
|
|
/>
|
|
</>);
|
|
});
|