import { type RefObject, useCallback, useLayoutEffect, useMemo, useState, } from 'react'; import type { ProjectResourceCanvasLayoutMode, ProjectResourceCanvasSection, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { clampProjectResourceSectionHeight, clampProjectResourceSectionZoom, defaultProjectResourceSectionHeight, type ProjectResourceSectionHeightAction, projectResourceSectionHeightBounds, projectResourceSectionHeightKey, type ProjectResourceSectionZoomAction, RESOURCE_SECTION_ZOOM_DEFAULT, RESOURCE_SECTION_ZOOM_MAX, RESOURCE_SECTION_ZOOM_MIN, updateProjectResourceSectionHeight, updateProjectResourceSectionZoom, } from './resourceSectionHeightModel'; const RESOURCE_LAYOUT_MODES: readonly ProjectResourceCanvasLayoutMode[] = [ 'dependency', 'type', ]; const RESOURCE_SECTIONS: readonly ProjectResourceCanvasSection[] = [ 'document', 'version', 'art', 'audio', ]; export type ProjectResourceSectionHeightState = { height: number; min: number; max: number; defaultHeight: number; atMin: boolean; atMax: boolean; atDefault: boolean; zoom: number; atMinZoom: boolean; atMaxZoom: boolean; atDefaultZoom: boolean; }; export function useProjectResourceSectionHeights(input: { projectId: string; mode: ProjectResourceCanvasLayoutMode; canvasRef: RefObject; enabled: boolean; }) { const { canvasRef, enabled, mode, projectId } = input; const [bounds, setBounds] = useState(() => projectResourceSectionHeightBounds(0), ); const [sessionHeights, setSessionHeights] = useState>( () => new Map(), ); const [sessionZooms, setSessionZooms] = useState>( () => new Map(), ); useLayoutEffect(() => { if (!enabled) { return undefined; } let frameId: number | null = null; const measure = () => { frameId = null; const canvasHeight = canvasRef.current?.clientHeight ?? 0; if (canvasHeight <= 0) { return; } const nextBounds = projectResourceSectionHeightBounds(canvasHeight); setBounds((current) => current.min === nextBounds.min && current.max === nextBounds.max ? current : nextBounds, ); setSessionHeights((current) => { let next: Map | null = null; for (const layoutMode of RESOURCE_LAYOUT_MODES) { for (const section of RESOURCE_SECTIONS) { const key = projectResourceSectionHeightKey({ projectId, mode: layoutMode, section, }); const height = current.get(key); if (height === undefined) { continue; } const clamped = clampProjectResourceSectionHeight( height, nextBounds, ); if (clamped !== height) { next ??= new Map(current); next.set(key, clamped); } } } return next ?? current; }); }; const scheduleMeasure = () => { if (frameId !== null) { return; } frameId = window.requestAnimationFrame(measure); }; measure(); window.addEventListener('resize', scheduleMeasure); return () => { if (frameId !== null) { window.cancelAnimationFrame(frameId); } window.removeEventListener('resize', scheduleMeasure); }; }, [canvasRef, enabled, projectId]); const sectionStates = useMemo(() => { const defaultHeight = defaultProjectResourceSectionHeight(bounds); return new Map( RESOURCE_SECTIONS.map((section) => { const key = projectResourceSectionHeightKey({ projectId, mode, section, }); const height = clampProjectResourceSectionHeight( sessionHeights.get(key) ?? defaultHeight, bounds, ); const zoom = clampProjectResourceSectionZoom( sessionZooms.get(key) ?? RESOURCE_SECTION_ZOOM_DEFAULT, ); return [ section, { height, min: bounds.min, max: bounds.max, defaultHeight, atMin: height === bounds.min, atMax: height === bounds.max, atDefault: height === defaultHeight, zoom, atMinZoom: zoom === RESOURCE_SECTION_ZOOM_MIN, atMaxZoom: zoom === RESOURCE_SECTION_ZOOM_MAX, atDefaultZoom: zoom === RESOURCE_SECTION_ZOOM_DEFAULT, } satisfies ProjectResourceSectionHeightState, ] as const; }), ); }, [bounds, mode, projectId, sessionHeights, sessionZooms]); const updateSectionHeight = useCallback( ( section: ProjectResourceCanvasSection, action: ProjectResourceSectionHeightAction, ) => { const key = projectResourceSectionHeightKey({ projectId, mode, section }); setSessionHeights((current) => { const currentHeight = clampProjectResourceSectionHeight( current.get(key) ?? defaultProjectResourceSectionHeight(bounds), bounds, ); const nextHeight = updateProjectResourceSectionHeight( currentHeight, action, bounds, ); if (nextHeight === currentHeight) { return current; } const next = new Map(current); next.set(key, nextHeight); return next; }); }, [bounds, mode, projectId], ); const setSectionZoom = useCallback( (section: ProjectResourceCanvasSection, zoom: number) => { const key = projectResourceSectionHeightKey({ projectId, mode, section }); const nextZoom = clampProjectResourceSectionZoom(zoom); setSessionZooms((current) => { const currentZoom = clampProjectResourceSectionZoom( current.get(key) ?? RESOURCE_SECTION_ZOOM_DEFAULT, ); if (nextZoom === currentZoom) { return current; } const next = new Map(current); next.set(key, nextZoom); return next; }); }, [mode, projectId], ); const updateSectionZoom = useCallback( ( section: ProjectResourceCanvasSection, action: ProjectResourceSectionZoomAction, ) => { const key = projectResourceSectionHeightKey({ projectId, mode, section }); setSessionZooms((current) => { const currentZoom = clampProjectResourceSectionZoom( current.get(key) ?? RESOURCE_SECTION_ZOOM_DEFAULT, ); const nextZoom = updateProjectResourceSectionZoom(currentZoom, action); if (nextZoom === currentZoom) { return current; } const next = new Map(current); next.set(key, nextZoom); return next; }); }, [mode, projectId], ); return { sectionStates, setSectionZoom, updateSectionHeight, updateSectionZoom, }; }