import { useEffect, useRef, useState } from "react"; import type { AppStore } from "@editor/store"; import type { GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index"; import { createViewportPointerPanHandler, handleViewportWheel, pointerInputEventFromPointerEvent, wheelInputEventFromWheelEvent, } from "@input/index"; import { createRenderer } from "@renderer/index"; const ignoreGlobalPointer: GlobalPointerConsumer = () => false; const ignoreGlobalWheel: GlobalWheelConsumer = () => false; export type CanvasViewportProps = { store: AppStore; globalPointerConsumer?: GlobalPointerConsumer; globalWheelConsumer?: GlobalWheelConsumer; }; export function CanvasViewport({ store, globalPointerConsumer = ignoreGlobalPointer, globalWheelConsumer = ignoreGlobalWheel, }: CanvasViewportProps) { const canvasRef = useRef(null); 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 = createViewportPointerPanHandler({ globalConsumer: globalPointerConsumer, dispatch: store.dispatch, getCurrentZoom: () => store.getState().editor.viewport.zoom, }); 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(); }; canvas.addEventListener("pointerdown", handlePointerDown); canvas.addEventListener("pointermove", handlePointerMove); canvas.addEventListener("pointerup", handlePointerUp); canvas.addEventListener("pointercancel", handlePointerUp); return () => { canvas.removeEventListener("pointerdown", handlePointerDown); canvas.removeEventListener("pointermove", handlePointerMove); canvas.removeEventListener("pointerup", handlePointerUp); canvas.removeEventListener("pointercancel", handlePointerUp); }; }, [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]); return ; }