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