Enhance App component with validation and UI updates for better user experience
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import type { Vec3 } from "../core/vector";
|
||||
import { DEFAULT_BOX_BRUSH_SIZE } from "../document/brushes";
|
||||
import type { SceneDocument, WorldSettings } from "../document/scene-document";
|
||||
import { DEFAULT_GRID_SIZE } from "../geometry/grid-snapping";
|
||||
|
||||
import { ViewportHost } from "./viewport-host";
|
||||
|
||||
@@ -9,13 +13,24 @@ interface ViewportCanvasProps {
|
||||
world: WorldSettings;
|
||||
sceneDocument: SceneDocument;
|
||||
selection: EditorSelection;
|
||||
onBrushSelectionChange(selection: EditorSelection): void;
|
||||
toolMode: ToolMode;
|
||||
onSelectionChange(selection: EditorSelection): void;
|
||||
onCreateBoxBrush(center: Vec3): void;
|
||||
}
|
||||
|
||||
export function ViewportCanvas({ world, sceneDocument, selection, onBrushSelectionChange }: ViewportCanvasProps) {
|
||||
function formatVec3(vector: Vec3 | null): string {
|
||||
if (vector === null) {
|
||||
return "Move over the grid to preview a snapped placement.";
|
||||
}
|
||||
|
||||
return `${vector.x}, ${vector.y}, ${vector.z}`;
|
||||
}
|
||||
|
||||
export function ViewportCanvas({ world, sceneDocument, selection, toolMode, onSelectionChange, onCreateBoxBrush }: ViewportCanvasProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const hostRef = useRef<ViewportHost | null>(null);
|
||||
const [viewportMessage, setViewportMessage] = useState<string | null>(null);
|
||||
const [boxCreatePreview, setBoxCreatePreview] = useState<Vec3 | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -61,11 +76,46 @@ export function ViewportCanvas({ world, sceneDocument, selection, onBrushSelecti
|
||||
}, [sceneDocument, selection]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setBrushSelectionChangeHandler(onBrushSelectionChange);
|
||||
}, [onBrushSelectionChange]);
|
||||
hostRef.current?.setBrushSelectionChangeHandler(onSelectionChange);
|
||||
}, [onSelectionChange]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setCreateBoxBrushHandler(onCreateBoxBrush);
|
||||
}, [onCreateBoxBrush]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setBoxCreatePreviewHandler(setBoxCreatePreview);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setToolMode(toolMode);
|
||||
|
||||
if (toolMode !== "box-create") {
|
||||
setBoxCreatePreview(null);
|
||||
}
|
||||
}, [toolMode]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="viewport-canvas" data-testid="viewport-shell" aria-label="Editor viewport">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`viewport-canvas viewport-canvas--${toolMode}`}
|
||||
data-testid="viewport-shell"
|
||||
aria-label="Editor viewport"
|
||||
>
|
||||
<div className="viewport-canvas__overlay" data-testid="viewport-overlay">
|
||||
<div className="viewport-canvas__overlay-badge">{toolMode === "box-create" ? "Box Create" : "Select"}</div>
|
||||
<div className="viewport-canvas__overlay-text">
|
||||
{toolMode === "box-create"
|
||||
? `Click to place a ${DEFAULT_BOX_BRUSH_SIZE.x} x ${DEFAULT_BOX_BRUSH_SIZE.y} x ${DEFAULT_BOX_BRUSH_SIZE.z} box on the ${DEFAULT_GRID_SIZE}m grid.`
|
||||
: "Click a brush, face, or Player Start marker to update the authored selection."}
|
||||
</div>
|
||||
{toolMode !== "box-create" ? null : (
|
||||
<div className="viewport-canvas__overlay-preview" data-testid="viewport-snap-preview">
|
||||
Next box center: {formatVec3(boxCreatePreview)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{viewportMessage === null ? null : (
|
||||
<div className="viewport-canvas__fallback" role="status">
|
||||
<div className="viewport-canvas__fallback-title">Viewport Unavailable</div>
|
||||
|
||||
@@ -404,6 +404,16 @@ export class ViewportHost {
|
||||
}
|
||||
|
||||
private handlePointerDown = (event: PointerEvent) => {
|
||||
if (this.toolMode === "box-create") {
|
||||
const previewCenter = this.getBoxCreatePreviewCenter(event);
|
||||
|
||||
if (previewCenter !== null) {
|
||||
this.createBoxBrushHandler?.(previewCenter);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = this.renderer.domElement.getBoundingClientRect();
|
||||
|
||||
if (bounds.width === 0 || bounds.height === 0) {
|
||||
@@ -464,6 +474,68 @@ export class ViewportHost {
|
||||
});
|
||||
};
|
||||
|
||||
private handlePointerMove = (event: PointerEvent) => {
|
||||
if (this.toolMode !== "box-create") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBoxCreatePreview(this.getBoxCreatePreviewCenter(event));
|
||||
};
|
||||
|
||||
private handlePointerLeave = () => {
|
||||
if (this.toolMode !== "box-create") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBoxCreatePreview(null);
|
||||
};
|
||||
|
||||
private getBoxCreatePreviewCenter(event: PointerEvent): Vec3 | null {
|
||||
const bounds = this.renderer.domElement.getBoundingClientRect();
|
||||
|
||||
if (bounds.width === 0 || bounds.height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.pointer.x = ((event.clientX - bounds.left) / bounds.width) * 2 - 1;
|
||||
this.pointer.y = -(((event.clientY - bounds.top) / bounds.height) * 2 - 1);
|
||||
this.raycaster.setFromCamera(this.pointer, this.camera);
|
||||
|
||||
if (this.raycaster.ray.intersectPlane(this.boxCreatePlane, this.boxCreateIntersection) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x: snapValueToGrid(this.boxCreateIntersection.x, DEFAULT_GRID_SIZE),
|
||||
y: DEFAULT_BOX_BRUSH_SIZE.y * 0.5,
|
||||
z: snapValueToGrid(this.boxCreateIntersection.z, DEFAULT_GRID_SIZE)
|
||||
};
|
||||
}
|
||||
|
||||
private setBoxCreatePreview(center: Vec3 | null) {
|
||||
if (
|
||||
(center === null && this.lastBoxCreatePreviewCenter === null) ||
|
||||
(center !== null &&
|
||||
this.lastBoxCreatePreviewCenter !== null &&
|
||||
center.x === this.lastBoxCreatePreviewCenter.x &&
|
||||
center.y === this.lastBoxCreatePreviewCenter.y &&
|
||||
center.z === this.lastBoxCreatePreviewCenter.z)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastBoxCreatePreviewCenter = center === null ? null : { ...center };
|
||||
this.boxCreatePreviewMesh.visible = center !== null;
|
||||
this.boxCreatePreviewEdges.visible = center !== null;
|
||||
|
||||
if (center !== null) {
|
||||
this.boxCreatePreviewMesh.position.set(center.x, center.y, center.z);
|
||||
this.boxCreatePreviewEdges.position.set(center.x, center.y, center.z);
|
||||
}
|
||||
|
||||
this.boxCreatePreviewHandler?.(this.lastBoxCreatePreviewCenter);
|
||||
}
|
||||
|
||||
private render = () => {
|
||||
this.animationFrame = window.requestAnimationFrame(this.render);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
Reference in New Issue
Block a user