blockly3d/src/components/ObjectEditorView.tsx

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}
/>
</>);
});