From ef05667caa9ea303173596eb90ea2a3d06981c92 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 5 Jun 2026 18:01:48 +0800 Subject: [PATCH] refactor: extract canvas derived state --- src/features/canvas/CanvasPage.tsx | 46 ++++-------- src/features/canvas/useCanvasDerivedState.ts | 74 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 src/features/canvas/useCanvasDerivedState.ts diff --git a/src/features/canvas/CanvasPage.tsx b/src/features/canvas/CanvasPage.tsx index 1dd22c8..b965207 100644 --- a/src/features/canvas/CanvasPage.tsx +++ b/src/features/canvas/CanvasPage.tsx @@ -55,6 +55,7 @@ import { useCanvasHistory, type CanvasHistorySnapshot } from "./useCanvasHistory import { useCanvasKeyboard } from "./useCanvasKeyboard"; import { useCanvasNodeDrag } from "./useCanvasNodeDrag"; import { useCanvasGeneration, addCanvasGenKeepalive, removeCanvasGenKeepalive } from "./useCanvasGeneration"; +import { useCanvasAssetSummary, useCanvasVisibleNodes } from "./useCanvasDerivedState"; import { toHappyHorseDisplayModel, } from "../../utils/happyHorseRouting"; @@ -578,17 +579,7 @@ function CanvasPage({ // Save immediately when user leaves page or switches tab (placed after runCanvasAutoSave definition) // — see useEffect below near runCanvasAutoSave - const canvasAssets = useMemo( - () => serverAssets.filter((asset) => asset.imageUrl), - [serverAssets], - ); - const assetCountsByCategory = useMemo(() => { - const counts = new Map(); - for (const asset of serverAssets) { - counts.set(asset.type, (counts.get(asset.type) ?? 0) + 1); - } - return counts; - }, [serverAssets]); + const { canvasAssets, assetCountsByCategory } = useCanvasAssetSummary(serverAssets); const shouldShowEmptyProjectState = projectsLoaded && projects.length === 0 && !projectId && workflow.source === "blank" && workflow.nodes.length === 0; const isWaitingForProjects = isAuthenticated && !projectsLoaded; @@ -2650,16 +2641,17 @@ function CanvasPage({ setConnectorDrag(null); }; - const collapsedPackageNodeKeys = useMemo( - () => new Set( - nodePackages.flatMap((nodePackage) => - nodePackage.collapsed ? nodePackage.nodeIds.map((node) => getCanvasSelectionKey(node)) : [] - ) - ), - [nodePackages], - ); - const isNodeCollapsedInPackage = (kind: CanvasNodeKind, id: string) => - collapsedPackageNodeKeys.has(getCanvasSelectionKey({ kind, id })); + const { + isNodeCollapsedInPackage, + visibleTextNodes, + visibleImageNodes, + visibleVideoNodes, + } = useCanvasVisibleNodes({ + textNodes, + imageNodes, + videoNodes, + nodePackages, + }); const isLinkCollapsedInPackage = (link: { sourceKind: CanvasNodeKind; sourceNodeId: string; targetKind: CanvasNodeKind; targetNodeId: string }) => isNodeCollapsedInPackage(link.sourceKind, link.sourceNodeId) || isNodeCollapsedInPackage(link.targetKind, link.targetNodeId); @@ -2697,18 +2689,6 @@ function CanvasPage({ return positionedLink ? [positionedLink] : []; }), ].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 = pendingLinkPort && pendingLinkPreviewPoint ? (() => { diff --git a/src/features/canvas/useCanvasDerivedState.ts b/src/features/canvas/useCanvasDerivedState.ts new file mode 100644 index 0000000..7e32226 --- /dev/null +++ b/src/features/canvas/useCanvasDerivedState.ts @@ -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(); + + 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, + }; +}