3 changed files with 115 additions and 118 deletions
@ -0,0 +1,95 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useLayoutEffect, useMemo, type CSSProperties } from "react"; |
||||
|
import { useNodeToolStore } from "@/store/nodeToolStore"; |
||||
|
import { useWorkflowStore } from "@/store/workflowStore"; |
||||
|
import { getWorkflowNodeStyleDimensions } from "@/utils/nodeDimensions"; |
||||
|
import type { WorkflowNode } from "@/types"; |
||||
|
|
||||
|
export interface NodeCanvasFrame { |
||||
|
node: WorkflowNode; |
||||
|
/** 节点样式盒子尺寸(= 显示态媒体框尺寸,媒体 h-full w-full 铺满节点)。 */ |
||||
|
styleDims: { width: number; height: number }; |
||||
|
/** 媒体自然尺寸(来自 node.data.dimensions),供 overlay letterbox;可能为 null。 */ |
||||
|
dimensions: { width: number; height: number } | null; |
||||
|
/** 画布级宿主定位样式:flow 坐标 + 父组偏移 + 样式尺寸,缩放/平移由 ViewportPortal 施加。 */ |
||||
|
frameStyle: CSSProperties; |
||||
|
} |
||||
|
|
||||
|
function asDimensions(value: unknown): { width: number; height: number } | null { |
||||
|
if (!value || typeof value !== "object") return null; |
||||
|
const dimensions = value as { width?: unknown; height?: unknown }; |
||||
|
return typeof dimensions.width === "number" && typeof dimensions.height === "number" |
||||
|
? { width: dimensions.width, height: dimensions.height } |
||||
|
: null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 画布级编辑宿主的统一定位 hook——图片与视频共用同一套 style 定位逻辑。 |
||||
|
* |
||||
|
* 前提:编辑工具仅在"有媒体的显示态"激活,此时媒体框 `h-full w-full` 铺满节点 |
||||
|
* (BaseNode fullBleed、无 header),故 `节点 flow 坐标 + 父组偏移 + 节点样式尺寸` |
||||
|
* 即媒体框在画布坐标中的框,无需量真实 DOM 矩形。overlay 内部再用自然尺寸做 |
||||
|
* letterbox。宿主渲染在 <ViewportPortal> 内,缩放/平移由 portal 父级 transform 统一施加。 |
||||
|
* |
||||
|
* 返回 null 表示当前不应渲染宿主(无目标节点或样式尺寸未就绪)。目标节点被删除时 |
||||
|
* 顺带清理悬空的工具状态。 |
||||
|
*/ |
||||
|
export function useNodeCanvasFrame(targetNodeId: string | null): NodeCanvasFrame | null { |
||||
|
const closeTool = useNodeToolStore((state) => state.closeTool); |
||||
|
|
||||
|
const node = useWorkflowStore((state) => |
||||
|
targetNodeId ? state.nodes.find((candidate) => candidate.id === targetNodeId) ?? null : null |
||||
|
); |
||||
|
|
||||
|
// 组内子节点的 position 是相对父组的偏移;宿主在 ViewportPortal 里是 portal 的
|
||||
|
// 直接子元素、不在组 DOM 内,需沿 parentId 累加父组坐标才是画布绝对位置。
|
||||
|
// 根节点无 parentId,偏移为 0。只读父链坐标(深度 1-2),不建 Map、不订阅全量。
|
||||
|
const parentOffsetX = useWorkflowStore((state) => { |
||||
|
let x = 0; |
||||
|
let parentId = node?.parentId ?? null; |
||||
|
while (parentId) { |
||||
|
const parent = state.nodes.find((candidate) => candidate.id === parentId); |
||||
|
if (!parent) break; |
||||
|
x += parent.position.x; |
||||
|
parentId = parent.parentId ?? null; |
||||
|
} |
||||
|
return x; |
||||
|
}); |
||||
|
const parentOffsetY = useWorkflowStore((state) => { |
||||
|
let y = 0; |
||||
|
let parentId = node?.parentId ?? null; |
||||
|
while (parentId) { |
||||
|
const parent = state.nodes.find((candidate) => candidate.id === parentId); |
||||
|
if (!parent) break; |
||||
|
y += parent.position.y; |
||||
|
parentId = parent.parentId ?? null; |
||||
|
} |
||||
|
return y; |
||||
|
}); |
||||
|
|
||||
|
const dimensions = useMemo(() => asDimensions(node?.data?.dimensions), [node?.data?.dimensions]); |
||||
|
const styleDims = node ? getWorkflowNodeStyleDimensions(node) : null; |
||||
|
|
||||
|
// 活动节点已被删除:清理悬空的工具状态。
|
||||
|
useLayoutEffect(() => { |
||||
|
if (targetNodeId && !node) { |
||||
|
closeTool(); |
||||
|
} |
||||
|
}, [closeTool, node, targetNodeId]); |
||||
|
|
||||
|
if (!node || !styleDims) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const frameStyle: CSSProperties = { |
||||
|
position: "absolute", |
||||
|
transform: `translate(${node.position.x + parentOffsetX}px, ${node.position.y + parentOffsetY}px)`, |
||||
|
width: styleDims.width, |
||||
|
height: styleDims.height, |
||||
|
zIndex: 10020, |
||||
|
pointerEvents: "none", |
||||
|
}; |
||||
|
|
||||
|
return { node, styleDims, dimensions, frameStyle }; |
||||
|
} |
||||
Loading…
Reference in new issue