import { useEffect, useRef, useState } from "react"; import type { LoadedModelAsset } from "../assets/gltf-model-import"; import type { LoadedImageAsset } from "../assets/image-assets"; import type { ProjectAssetRecord } from "../assets/project-assets"; import type { EditorSelection } from "../core/selection"; import type { ToolMode } from "../core/tool-mode"; import type { Vec3 } from "../core/vector"; import { DEFAULT_BOX_BRUSH_CENTER, DEFAULT_BOX_BRUSH_SIZE } from "../document/brushes"; import type { SceneDocument } from "../document/scene-document"; import type { WorldSettings } from "../document/world-settings"; import { createWorldBackgroundStyle } from "../shared-ui/world-background-style"; import { getViewportPanelLabel, type ViewportDisplayMode, type ViewportLayoutMode, type ViewportPanelId } from "./viewport-layout"; import { getViewportViewModeControlHint, getViewportViewModeGridPlaneLabel, getViewportViewModeLabel, type ViewportViewMode } from "./viewport-view-modes"; import type { PlacementViewportToolPreview, ViewportToolPreview } from "./viewport-transient-state"; import { ViewportHost } from "./viewport-host"; interface ViewportCanvasProps { panelId: ViewportPanelId; world: WorldSettings; sceneDocument: SceneDocument; projectAssets: Record; loadedModelAssets: Record; loadedImageAssets: Record; selection: EditorSelection; toolMode: ToolMode; toolPreview: ViewportToolPreview; viewMode: ViewportViewMode; displayMode: ViewportDisplayMode; layoutMode: ViewportLayoutMode; isActivePanel: boolean; focusRequestId: number; focusSelection: EditorSelection; onSelectionChange(selection: EditorSelection): void; onCreateBoxBrush(center: Vec3): void; onCommitPlacement(toolPreview: PlacementViewportToolPreview): void; onToolPreviewChange(toolPreview: ViewportToolPreview): void; } function formatVec3(vector: Vec3): string { return `${vector.x}, ${vector.y}, ${vector.z}`; } function getViewportOverlayText( toolMode: ToolMode, viewMode: ViewportViewMode, displayMode: ViewportDisplayMode, layoutMode: ViewportLayoutMode ): string | null { if (layoutMode === "quad") { return null; } if (toolMode === "box-create") { return `Hover the ${getViewportViewModeGridPlaneLabel(viewMode)} grid to place a ${DEFAULT_BOX_BRUSH_SIZE.x} x ${DEFAULT_BOX_BRUSH_SIZE.y} x ${DEFAULT_BOX_BRUSH_SIZE.z} box. ${getViewportViewModeControlHint(viewMode)}`; } if (toolMode === "place") { return `Hover the ${getViewportViewModeGridPlaneLabel(viewMode)} grid to position the preview. Click to place it. ${getViewportViewModeControlHint(viewMode)}`; } return `${displayMode === "authoring" ? "Authoring view" : `${getViewportViewModeLabel(viewMode)} view`} on the ${getViewportViewModeGridPlaneLabel(viewMode)} grid. ${getViewportViewModeControlHint(viewMode)}`; } export function ViewportCanvas({ panelId, world, sceneDocument, projectAssets, loadedModelAssets, loadedImageAssets, selection, toolMode, toolPreview, viewMode, displayMode, layoutMode, isActivePanel, focusRequestId, focusSelection, onSelectionChange, onCreateBoxBrush, onCommitPlacement, onToolPreviewChange }: ViewportCanvasProps) { const containerRef = useRef(null); const hostRef = useRef(null); const [viewportMessage, setViewportMessage] = useState(null); useEffect(() => { const container = containerRef.current; if (container === null) { return; } const testCanvas = document.createElement("canvas"); const hasWebGl = testCanvas.getContext("webgl2") !== null || testCanvas.getContext("webgl") !== null || testCanvas.getContext("experimental-webgl") !== null; if (!hasWebGl) { setViewportMessage("WebGL is unavailable in this browser environment. The viewport shell is visible, but rendering is disabled."); return; } try { const viewportHost = new ViewportHost(); hostRef.current = viewportHost; viewportHost.mount(container); setViewportMessage(null); return () => { viewportHost.dispose(); hostRef.current = null; }; } catch (error) { const message = error instanceof Error ? error.message : "Viewport initialization failed."; setViewportMessage(`Viewport initialization failed: ${message}`); return; } }, []); useEffect(() => { hostRef.current?.updateWorld(world); }, [world]); useEffect(() => { hostRef.current?.updateAssets(projectAssets, loadedModelAssets, loadedImageAssets); }, [projectAssets, loadedModelAssets, loadedImageAssets]); useEffect(() => { hostRef.current?.updateDocument(sceneDocument, selection); }, [sceneDocument, selection]); useEffect(() => { hostRef.current?.setViewMode(viewMode); }, [viewMode]); useEffect(() => { hostRef.current?.setDisplayMode(displayMode); }, [displayMode]); useEffect(() => { hostRef.current?.setBrushSelectionChangeHandler(onSelectionChange); }, [onSelectionChange]); useEffect(() => { hostRef.current?.setCreateBoxBrushHandler(onCreateBoxBrush); }, [onCreateBoxBrush]); useEffect(() => { hostRef.current?.setBoxCreatePreviewChangeHandler((center) => { onToolPreviewChange( center === null ? { kind: "none" } : { kind: "box-create", sourcePanelId: panelId, center } ); }); }, [onToolPreviewChange, panelId]); useEffect(() => { hostRef.current?.setPlacementPreviewChangeHandler(onToolPreviewChange); }, [onToolPreviewChange]); useEffect(() => { hostRef.current?.setPlacementCommitHandler(onCommitPlacement); }, [onCommitPlacement]); useEffect(() => { hostRef.current?.setToolMode(toolMode); }, [toolMode]); useEffect(() => { hostRef.current?.setBoxCreatePreview(toolMode === "box-create" && toolPreview.kind === "box-create" ? toolPreview.center : null); }, [toolMode, toolPreview]); useEffect(() => { hostRef.current?.setPlacementPreview(toolMode === "place" && toolPreview.kind === "placement" ? toolPreview : null); }, [toolMode, toolPreview]); useEffect(() => { if (focusRequestId === 0) { return; } hostRef.current?.focusSelection(sceneDocument, focusSelection); }, [focusRequestId, focusSelection, sceneDocument]); const overlayText = getViewportOverlayText(toolMode, viewMode, displayMode, layoutMode); const previewVisible = (toolMode === "box-create" && toolPreview.kind === "box-create" && toolPreview.center !== null) || (toolMode === "place" && toolPreview.kind === "placement" && toolPreview.center !== null); return (
{toolMode === "box-create" ? "Box Create" : toolMode === "place" ? "Place" : "Select"}
{getViewportViewModeLabel(viewMode)}
{displayMode === "authoring" ? "Authoring" : "Lit"}
{overlayText === null ? null :
{overlayText}
} {!previewVisible ? null : (
Preview: {formatVec3(toolPreview.center)}
)}
{viewportMessage === null ? null : (
Viewport Unavailable
{viewportMessage}
{toolMode !== "box-create" && toolMode !== "place" ? null : ( )}
)}
); }