refactor: extract canvas derived state

This commit is contained in:
2026-06-05 18:01:48 +08:00
parent b8b3b8f137
commit ef05667caa
2 changed files with 87 additions and 33 deletions
+13 -33
View File
@@ -55,6 +55,7 @@ import { useCanvasHistory, type CanvasHistorySnapshot } from "./useCanvasHistory
import { useCanvasKeyboard } from "./useCanvasKeyboard"; import { useCanvasKeyboard } from "./useCanvasKeyboard";
import { useCanvasNodeDrag } from "./useCanvasNodeDrag"; import { useCanvasNodeDrag } from "./useCanvasNodeDrag";
import { useCanvasGeneration, addCanvasGenKeepalive, removeCanvasGenKeepalive } from "./useCanvasGeneration"; import { useCanvasGeneration, addCanvasGenKeepalive, removeCanvasGenKeepalive } from "./useCanvasGeneration";
import { useCanvasAssetSummary, useCanvasVisibleNodes } from "./useCanvasDerivedState";
import { import {
toHappyHorseDisplayModel, toHappyHorseDisplayModel,
} from "../../utils/happyHorseRouting"; } from "../../utils/happyHorseRouting";
@@ -578,17 +579,7 @@ function CanvasPage({
// Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition) // Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition)
// — see useEffect below near runCanvasAutoSave // — see useEffect below near runCanvasAutoSave
const canvasAssets = useMemo( const { canvasAssets, assetCountsByCategory } = useCanvasAssetSummary(serverAssets);
() => serverAssets.filter((asset) => asset.imageUrl),
[serverAssets],
);
const assetCountsByCategory = useMemo(() => {
const counts = new Map<string, number>();
for (const asset of serverAssets) {
counts.set(asset.type, (counts.get(asset.type) ?? 0) + 1);
}
return counts;
}, [serverAssets]);
const shouldShowEmptyProjectState = const shouldShowEmptyProjectState =
projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0; projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0;
const isWaitingForProjects = isAuthenticated && !projectsLoaded; const isWaitingForProjects = isAuthenticated && !projectsLoaded;
@@ -2650,16 +2641,17 @@ function CanvasPage({
setConnectorDrag(null); setConnectorDrag(null);
}; };
const collapsedPackageNodeKeys = useMemo( const {
() => new Set( isNodeCollapsedInPackage,
nodePackages.flatMap((nodePackage) => visibleTextNodes,
nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : [] visibleImageNodes,
) visibleVideoNodes,
), } = useCanvasVisibleNodes({
[nodePackages], textNodes,
); imageNodes,
const isNodeCollapsedInPackage = (kind: CanvasNodeKind, id: string) => videoNodes,
collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id })); nodePackages,
});
const isLinkCollapsedInPackage = (link: { sourceKind: CanvasNodeKind; sourceNodeId: string; targetKind: CanvasNodeKind; targetNodeId: string }) => const isLinkCollapsedInPackage = (link: { sourceKind: CanvasNodeKind; sourceNodeId: string; targetKind: CanvasNodeKind; targetNodeId: string }) =>
isNodeCollapsedInPackage(link.sourceKind, link.sourceNodeId) || isNodeCollapsedInPackage(link.sourceKind, link.sourceNodeId) ||
isNodeCollapsedInPackage(link.targetKind, link.targetNodeId); isNodeCollapsedInPackage(link.targetKind, link.targetNodeId);
@@ -2697,18 +2689,6 @@ function CanvasPage({
return positionedLink ? [positionedLink] : []; return positionedLink ? [positionedLink] : [];
}), }),
].filter((link) => !isLinkCollapsedInPackage(link)); ].filter((link) => !isLinkCollapsedInPackage(link));
const visibleTextNodes = useMemo(
() => textNodes.filter((textNode) => !isNodeCollapsedInPackage("text", textNode.id)),
[collapsedPackageNodeKeys, textNodes],
);
const visibleImageNodes = useMemo(
() => imageNodes.filter((imageNode) => !isNodeCollapsedInPackage("image", imageNode.id)),
[collapsedPackageNodeKeys, imageNodes],
);
const visibleVideoNodes = useMemo(
() => videoNodes.filter((videoNode) => !isNodeCollapsedInPackage("video", videoNode.id)),
[collapsedPackageNodeKeys, videoNodes],
);
const pendingLinkPreview = const pendingLinkPreview =
pendingLinkPort && pendingLinkPreviewPoint pendingLinkPort && pendingLinkPreviewPoint
? (() => { ? (() => {
@@ -0,0 +1,74 @@
import { useCallback, useMemo } from "react";
import type { ServerAssetItem } from "../../api/assetClient";
import type {
CanvasImageNode,
CanvasNodeKind,
CanvasNodePackage,
CanvasTextNode,
CanvasVideoNode,
} from "./canvasTypes";
import { getCanvasSelectionKey } from "./canvasUtils";
export function useCanvasAssetSummary(serverAssets: ServerAssetItem[]) {
return useMemo(() => {
const canvasAssets: ServerAssetItem[] = [];
const assetCountsByCategory = new Map<string, number>();
for (const asset of serverAssets) {
if (asset.imageUrl) {
canvasAssets.push(asset);
}
assetCountsByCategory.set(asset.type, (assetCountsByCategory.get(asset.type) ?? 0) + 1);
}
return { canvasAssets, assetCountsByCategory };
}, [serverAssets]);
}
export function useCanvasVisibleNodes({
textNodes,
imageNodes,
videoNodes,
nodePackages,
}: {
textNodes: CanvasTextNode[];
imageNodes: CanvasImageNode[];
videoNodes: CanvasVideoNode[];
nodePackages: CanvasNodePackage[];
}) {
const collapsedPackageNodeKeys = useMemo(
() => new Set(
nodePackages.flatMap((nodePackage) =>
nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : []
)
),
[nodePackages],
);
const isNodeCollapsedInPackage = useCallback(
(kind: CanvasNodeKind, id: string) =>
collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id })),
[collapsedPackageNodeKeys],
);
const visibleTextNodes = useMemo(
() => textNodes.filter((textNode) => !collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind: "text", id: textNode.id }))),
[collapsedPackageNodeKeys, textNodes],
);
const visibleImageNodes = useMemo(
() => imageNodes.filter((imageNode) => !collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind: "image", id: imageNode.id }))),
[collapsedPackageNodeKeys, imageNodes],
);
const visibleVideoNodes = useMemo(
() => videoNodes.filter((videoNode) => !collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind: "video", id: videoNode.id }))),
[collapsedPackageNodeKeys, videoNodes],
);
return {
collapsedPackageNodeKeys,
isNodeCollapsedInPackage,
visibleTextNodes,
visibleImageNodes,
visibleVideoNodes,
};
}