mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 15:24:06 +08:00
cf1ea2646f
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>
92 lines
4.9 KiB
TypeScript
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);
|
|
}
|