Files
Genarrative/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceSectionHeights.ts
T
menghao ffb4cb5d62 完善资源管理工作台与有界预览
将资源卡本体化并补齐分区缩放滚动与依赖聚类

增加全进程三槽预览调度范围取消与安全分块读取

保持布局 CAS 预览缓存回收和依赖图权威合同

补齐资源管理前端 Tauri AppSurface 回归并同步文档
2026-08-11 18:07:14 +08:00

240 lines
6.8 KiB
TypeScript

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<HTMLDivElement | null>;
enabled: boolean;
}) {
const { canvasRef, enabled, mode, projectId } = input;
const [bounds, setBounds] = useState(() =>
projectResourceSectionHeightBounds(0),
);
const [sessionHeights, setSessionHeights] = useState<Map<string, number>>(
() => new Map(),
);
const [sessionZooms, setSessionZooms] = useState<Map<string, number>>(
() => 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<string, number> | 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,
};
}