ffb4cb5d62
将资源卡本体化并补齐分区缩放滚动与依赖聚类 增加全进程三槽预览调度范围取消与安全分块读取 保持布局 CAS 预览缓存回收和依赖图权威合同 补齐资源管理前端 Tauri AppSurface 回归并同步文档
240 lines
6.8 KiB
TypeScript
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,
|
|
};
|
|
}
|