67 lines
3.9 KiB
TypeScript
67 lines
3.9 KiB
TypeScript
/**
|
|
* 旧平面frameId成员关系的几何规则。内容放下才按中心点重新归组,分区移动带动成员,缩放不自动重分组。
|
|
* 这是单board的旧分区外壳逻辑,与schema v2子board树和新版ownerCanvasId层级分别处理。
|
|
*/
|
|
import type { NodeChange } from "@xyflow/react";
|
|
import { applyNodeChanges } from "@xyflow/react";
|
|
import type { CanvasNode } from "./canvas-elements.ts";
|
|
import { nodeCenter, nodeSize } from "./canvas-geometry.ts";
|
|
|
|
/** Membership changes only on content drops, never on frame movement or resize. */
|
|
/** 非frame节点按中心点找包含它的分区,多个重叠候选选面积最小者;frame本身不成为另一frame成员。 */
|
|
export function containingFrame(node: CanvasNode, nodes: CanvasNode[]) {
|
|
if (node.data.kind === "frame") return undefined;
|
|
// 以未旋转布局矩形判包含,边界算内部;不是对旋转后的可见形状做精确命中。
|
|
const center = nodeCenter(node);
|
|
return nodes.filter((frame) => {
|
|
if (frame.data.kind !== "frame") return false;
|
|
const { width, height } = nodeSize(frame);
|
|
return center.x >= frame.position.x && center.x <= frame.position.x + width && center.y >= frame.position.y && center.y <= frame.position.y + height;
|
|
}).sort((a, b) => nodeSize(a).width * nodeSize(a).height - nodeSize(b).width * nodeSize(b).height)[0];
|
|
}
|
|
|
|
/** 纯计算选中内容的from/to反馈,尚未改frameId;用于拖动结束时判断归属变动。 */
|
|
export function frameDropFeedback(nodes: CanvasNode[], droppedIds: string[]) {
|
|
const ids = new Set(droppedIds);
|
|
return nodes.filter((node) => ids.has(node.id) && node.data.kind !== "frame").map((node) => ({
|
|
id: node.id, from: node.data.frameId, to: containingFrame(node, nodes)?.id,
|
|
}));
|
|
}
|
|
|
|
/** 仅改droppedIds中非frame节点,归属没变复用原引用,移出所有frame时删除frameId字段。 */
|
|
export function assignFrameMembership(nodes: CanvasNode[], droppedIds: string[]): CanvasNode[] {
|
|
const dropped = new Set(droppedIds);
|
|
return nodes.map((node) => {
|
|
if (!dropped.has(node.id) || node.data.kind === "frame") return node;
|
|
const frameId = containingFrame(node, nodes)?.id;
|
|
if (frameId === node.data.frameId) return node;
|
|
const { frameId: _old, ...data } = node.data;
|
|
return { ...node, data: { ...data, ...(frameId ? { frameId } : {}) } };
|
|
});
|
|
}
|
|
|
|
/** 先用React Flow归约节点变更,再按旧成员关系传播分区平移。
|
|
* 同时明确移动的子节点不再叠加父位移,防选中父子一起拖时移动两次。 */
|
|
export function applyFrameNodeChanges(changes: NodeChange<CanvasNode>[], previous: CanvasNode[]) {
|
|
const nodes = applyNodeChanges(changes, previous);
|
|
const before = new Map(previous.map((node) => [node.id, node]));
|
|
// 仅带实际 position 值的变更计入显式移动;dragging 标记变化本身不触发成员平移。
|
|
const positioned = new Set(changes.flatMap((change) => change.type === "position" && change.position ? [change.id] : []));
|
|
const frames = new Map(nodes.filter((node) => node.data.kind === "frame").map((node) => [node.id, node]));
|
|
// 单次 map 按原 frameId 计算直接父位移,不递归累积嵌套分区;合法旧数据约定 frame 不归另一 frame。
|
|
return nodes.map((node) => {
|
|
const frameId = node.data.frameId;
|
|
if (!frameId) return node;
|
|
const frame = frames.get(frameId);
|
|
// frame被删除只撤销成员标记,不顺带删除内容节点。
|
|
if (!frame) {
|
|
const { frameId: _old, ...data } = node.data;
|
|
return { ...node, data };
|
|
}
|
|
// 只有分区收到明确position变化才传播;resize或新建分区不移动成员。
|
|
const oldFrame = before.get(frameId);
|
|
if (!oldFrame || !positioned.has(frameId) || positioned.has(node.id)) return node;
|
|
return { ...node, position: { x: node.position.x + frame.position.x - oldFrame.position.x, y: node.position.y + frame.position.y - oldFrame.position.y } };
|
|
});
|
|
}
|