Files
infinite-canvas/web/src/lib/canvas/canvas-node-geometry.ts
T
HouYunFei cf1ea2646f refactor(canvas-project): extract pure helpers into @/lib/canvas modules
Move the ~30 module-scope pure functions out of the 3.5k-line project.tsx into
three focused lib modules, with identical names and signatures. No behavior
change; project.tsx imports them back.

- canvas-node-factory.ts: createCanvasNode, *Metadata builders, referenceUrl,
  applyNodeConfigPatch.
- canvas-node-geometry.ts: nodeBounds, group drop/snap, connection anchors,
  normalizeConnection, hidden-batch checks.
- canvas-generation-helpers.ts: generation config/inputs, reference hydration,
  retry-source lookup, angle prompt builders, misc pure helpers.

project.tsx drops from 3536 to ~3230 lines. Also removes imports that were only
used by the moved helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:46:07 +08:00

92 lines
4.9 KiB
TypeScript

import { CanvasNodeType, type CanvasNodeData, type ConnectionHandle } from "@/types/canvas";
export function nodeBounds(nodes: CanvasNodeData[]) {
return nodes.reduce(
(acc, node) => ({
left: Math.min(acc.left, node.position.x),
top: Math.min(acc.top, node.position.y),
right: Math.max(acc.right, node.position.x + node.width),
bottom: Math.max(acc.bottom, node.position.y + node.height),
}),
{ left: Infinity, top: Infinity, right: -Infinity, bottom: -Infinity },
);
}
export function findGroupDropTarget(movedIds: Set<string>, nodes: CanvasNodeData[]) {
if (nodes.some((node) => movedIds.has(node.id) && node.type === CanvasNodeType.Group)) return null;
const movingNodes = nodes.filter((node) => movedIds.has(node.id) && node.type !== CanvasNodeType.Group);
if (!movingNodes.length) return null;
return (
[...nodes].reverse().find((group) => {
if (group.type !== CanvasNodeType.Group || movedIds.has(group.id)) return false;
return movingNodes.some((node) => {
const centerX = node.position.x + node.width / 2;
const centerY = node.position.y + node.height / 2;
return centerX >= group.position.x && centerX <= group.position.x + group.width && centerY >= group.position.y && centerY <= group.position.y + group.height;
});
}) || null
);
}
export function snapNodesIntoGroup(movedIds: Set<string>, nodes: CanvasNodeData[], group: CanvasNodeData) {
const movingNodes = nodes.filter((node) => movedIds.has(node.id) && node.type !== CanvasNodeType.Group);
if (!movingNodes.length) return nodes;
const pad = 24;
const bounds = nodeBounds(movingNodes);
const left = group.position.x + pad;
const top = group.position.y + pad;
const right = group.position.x + group.width - pad;
const bottom = group.position.y + group.height - pad;
const dx = bounds.right - bounds.left > right - left ? left - bounds.left : bounds.left < left ? left - bounds.left : bounds.right > right ? right - bounds.right : 0;
const dy = bounds.bottom - bounds.top > bottom - top ? top - bounds.top : bounds.top < top ? top - bounds.top : bounds.bottom > bottom ? bottom - bounds.bottom : 0;
return nodes.map((node) => {
if (!movedIds.has(node.id) || node.type === CanvasNodeType.Group) return node;
return { ...node, position: { x: node.position.x + dx, y: node.position.y + dy }, metadata: { ...node.metadata, groupId: group.id } };
});
}
export function findContainingGroupId(node: CanvasNodeData, nodes: CanvasNodeData[]) {
const centerX = node.position.x + node.width / 2;
const centerY = node.position.y + node.height / 2;
return (
[...nodes]
.reverse()
.find((group) => group.type === CanvasNodeType.Group && group.id !== node.id && centerX >= group.position.x && centerX <= group.position.x + group.width && centerY >= group.position.y && centerY <= group.position.y + group.height)?.id ||
undefined
);
}
export function getConnectionTargetAnchor(node: CanvasNodeData, current: ConnectionHandle) {
return {
x: current.handleType === "source" ? node.position.x : node.position.x + node.width,
y: node.position.y + node.height / 2,
};
}
export function normalizeConnection(firstNodeId: string, secondNodeId: string, nodes: CanvasNodeData[], firstHandleType: "source" | "target") {
const first = nodes.find((node) => node.id === firstNodeId);
const second = nodes.find((node) => node.id === secondNodeId);
if (!first || !second || first.id === second.id) return null;
if (first.type === CanvasNodeType.Group || second.type === CanvasNodeType.Group) return null;
if (first.type === CanvasNodeType.Config && second.type === CanvasNodeType.Config) return null;
if (second.type === CanvasNodeType.Config) return { fromNodeId: first.id, toNodeId: second.id };
if (first.type === CanvasNodeType.Config && firstHandleType === "target") return { fromNodeId: second.id, toNodeId: first.id };
if (first.type === CanvasNodeType.Config) return { fromNodeId: first.id, toNodeId: second.id };
return { fromNodeId: first.id, toNodeId: second.id };
}
export function isHiddenBatchChild(node: CanvasNodeData, nodes: CanvasNodeData[], collapsingBatchIds?: Set<string>) {
const rootId = node.metadata?.batchRootId;
if (!rootId) return false;
const root = nodes.find((item) => item.id === rootId);
if (root && collapsingBatchIds?.has(rootId)) return false;
return Boolean(root && !root.metadata?.imageBatchExpanded);
}
export function isHiddenBatchConnectionEndpoint(node: CanvasNodeData, nodes: CanvasNodeData[]) {
const rootId = node.metadata?.batchRootId;
if (!rootId) return false;
const root = nodes.find((item) => item.id === rootId);
return Boolean(root && !root.metadata?.imageBatchExpanded);
}