import { useEffect, useRef, useState } from "react"; import type { AppStore } from "@editor/store"; import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index"; import { createViewportPanInputController, handleViewportWheel, keybindEventFromKeyboardEvent, pointerInputEventFromPointerEvent, wheelInputEventFromWheelEvent, } from "@input/index"; import { createRenderer } from "@renderer/index"; import { useAppState } from "./useAppState"; const ignoreGlobalKeybind: GlobalKeybindConsumer = () => false; const ignoreGlobalPointer: GlobalPointerConsumer = () => false; const ignoreGlobalWheel: GlobalWheelConsumer = () => false; export type CanvasViewportProps = { store: AppStore; globalKeybindConsumer?: GlobalKeybindConsumer; globalPointerConsumer?: GlobalPointerConsumer; globalWheelConsumer?: GlobalWheelConsumer; }; export function CanvasViewport({ store, globalKeybindConsumer = ignoreGlobalKeybind, globalPointerConsumer = ignoreGlobalPointer, globalWheelConsumer = ignoreGlobalWheel, }: CanvasViewportProps) { const canvasRef = useRef(null); const state = useAppState(store); const [isPanning, setIsPanning] = useState(false); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const renderer = createRenderer(canvas); renderer.render(store.getState()); const unsubscribe = store.subscribe((state) => { renderer.render(state); }); return () => { unsubscribe(); renderer.dispose(); }; }, [store]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const resizeObserver = new ResizeObserver(([entry]) => { if (!entry) return; const width = Math.floor(entry.contentRect.width); const height = Math.floor(entry.contentRect.height); store.dispatch("viewport.setSize", { w: width, h: height }); }); resizeObserver.observe(canvas); return () => resizeObserver.disconnect(); }, [store]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const panHandler = createViewportPanInputController({ globalKeyConsumer: globalKeybindConsumer, globalPointerConsumer, dispatch: store.dispatch, getCurrentZoom: () => store.getState().editor.viewport.zoom, isPanMode: () => store.getState().editor.tools.interactionMode.type === "temporary-pan", }); const handleKeyDown = (event: KeyboardEvent) => { const consumed = panHandler.keyDown(keybindEventFromKeyboardEvent(event)); if (consumed) event.preventDefault(); }; const handleKeyUp = (event: KeyboardEvent) => { const consumed = panHandler.keyUp(keybindEventFromKeyboardEvent(event)); if (consumed) event.preventDefault(); }; const handlePointerDown = (event: PointerEvent) => { const consumed = panHandler.pointerDown(pointerInputEventFromPointerEvent(event)); if (!consumed) return; canvas.setPointerCapture(event.pointerId); setIsPanning(true); event.preventDefault(); }; const handlePointerMove = (event: PointerEvent) => { const consumed = panHandler.pointerMove(pointerInputEventFromPointerEvent(event)); if (consumed) event.preventDefault(); }; const handlePointerUp = (event: PointerEvent) => { const consumed = panHandler.pointerUp(pointerInputEventFromPointerEvent(event)); if (!consumed) return; setIsPanning(false); event.preventDefault(); }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); canvas.addEventListener("pointerdown", handlePointerDown); canvas.addEventListener("pointermove", handlePointerMove); canvas.addEventListener("pointerup", handlePointerUp); canvas.addEventListener("pointercancel", handlePointerUp); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); canvas.removeEventListener("pointerdown", handlePointerDown); canvas.removeEventListener("pointermove", handlePointerMove); canvas.removeEventListener("pointerup", handlePointerUp); canvas.removeEventListener("pointercancel", handlePointerUp); }; }, [globalKeybindConsumer, globalPointerConsumer, store]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const handleWheel = (event: WheelEvent) => { const consumed = handleViewportWheel({ event: wheelInputEventFromWheelEvent(event), globalConsumer: globalWheelConsumer, dispatch: store.dispatch, currentZoom: store.getState().editor.viewport.zoom, }); if (consumed) event.preventDefault(); }; canvas.addEventListener("wheel", handleWheel, { passive: false }); return () => canvas.removeEventListener("wheel", handleWheel); }, [globalWheelConsumer, store]); const isPanMode = state.editor.tools.interactionMode.type === "temporary-pan"; const cursorClass = isPanning ? "cursor-grabbing" : isPanMode ? "cursor-grab" : "cursor-default"; return ; }