Browse Source

修改icon,画布整理修改

feature/0710
yun 4 days ago
parent
commit
5f9eaf3af4
  1. BIN
      public/banana_icon.png
  2. BIN
      src/app/icon.png
  3. 2
      src/components/media/MediaOutpaintingOverlay.tsx
  4. 2
      src/components/nodes/GenerateImageNode.tsx
  5. 2
      src/components/nodes/ImageInputNode.tsx
  6. 23
      src/utils/__tests__/canvasAutoLayout.test.ts
  7. 136
      src/utils/canvasAutoLayout.ts

BIN
public/banana_icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 158 KiB

BIN
src/app/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 158 KiB

2
src/components/media/MediaOutpaintingOverlay.tsx

@ -57,7 +57,7 @@ function clamp(value: number, min: number, max: number) {
} }
function roundRatio(value: number) { function roundRatio(value: number) {
return Math.round(value * 1000) / 1000; return Math.round(value * 10) / 10;
} }
function frameFromBounds(bounds: DisplayRect): OutpaintingFrame { function frameFromBounds(bounds: DisplayRect): OutpaintingFrame {

2
src/components/nodes/GenerateImageNode.tsx

@ -136,7 +136,7 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle =
const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false); const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false);
const [activeEditMenuTool, setActiveEditMenuTool] = useState<MediaEditMenuTool>("edit"); const [activeEditMenuTool, setActiveEditMenuTool] = useState<MediaEditMenuTool>("crop");
const [cropAspectRatio, setCropAspectRatio] = useState<MediaCropAspectRatio>("original"); const [cropAspectRatio, setCropAspectRatio] = useState<MediaCropAspectRatio>("original");
const [cropPayload, setCropPayload] = useState<MediaCropConfirmPayload | null>(null); const [cropPayload, setCropPayload] = useState<MediaCropConfirmPayload | null>(null);
const [cropFrameRect, setCropFrameRect] = useState<MediaEditFrameRect | null>(null); const [cropFrameRect, setCropFrameRect] = useState<MediaEditFrameRect | null>(null);

2
src/components/nodes/ImageInputNode.tsx

@ -133,7 +133,7 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false);
const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false); const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false);
const [activeEditMenuTool, setActiveEditMenuTool] = useState<MediaEditMenuTool>("edit"); const [activeEditMenuTool, setActiveEditMenuTool] = useState<MediaEditMenuTool>("crop");
const [splitGridSelection, setSplitGridSelection] = useState<SplitGridSelection | null>(null); const [splitGridSelection, setSplitGridSelection] = useState<SplitGridSelection | null>(null);
const [imageRetryNonce, setImageRetryNonce] = useState(0); const [imageRetryNonce, setImageRetryNonce] = useState(0);
const [displayFallbackImage, setDisplayFallbackImage] = useState<string | null>(null); const [displayFallbackImage, setDisplayFallbackImage] = useState<string | null>(null);

23
src/utils/__tests__/canvasAutoLayout.test.ts

@ -101,7 +101,7 @@ describe("calculateCanvasAutoLayout", () => {
expect(positions.get("left-2")?.y).toBe(244); expect(positions.get("left-2")?.y).toBe(244);
}); });
it("uses global columns for separate workflows instead of separate component blocks", () => { it("keeps separate workflows on separate rows", () => {
const positions = positionMap(calculateCanvasAutoLayout( const positions = positionMap(calculateCanvasAutoLayout(
[ [
node("input-a", 100, 100), node("input-a", 100, 100),
@ -114,7 +114,26 @@ describe("calculateCanvasAutoLayout", () => {
expect(positions.get("input-a")?.x).toBe(positions.get("input-b")?.x); expect(positions.get("input-a")?.x).toBe(positions.get("input-b")?.x);
expect(positions.get("output-a")?.x).toBe(positions.get("output-b")?.x); expect(positions.get("output-a")?.x).toBe(positions.get("output-b")?.x);
expect(positions.get("input-a")?.y).toBe(positions.get("output-a")?.y);
expect(positions.get("input-b")?.y).toBe(positions.get("output-b")?.y);
expect(positions.get("input-a")?.y).toBeLessThan(positions.get("input-b")?.y ?? 0); expect(positions.get("input-a")?.y).toBeLessThan(positions.get("input-b")?.y ?? 0);
expect(positions.get("output-a")?.y).toBeLessThan(positions.get("output-b")?.y ?? 0); });
it("keeps isolated nodes as their own workflow rows", () => {
const positions = positionMap(calculateCanvasAutoLayout(
[
node("image-input", 100, 100),
node("image-output", 600, 100),
node("text", 100, 350),
node("video-input", 100, 700),
node("video-output", 600, 700),
],
[edge("image-input", "image-output"), edge("video-input", "video-output")]
));
expect(positions.get("image-input")?.y).toBe(positions.get("image-output")?.y);
expect(positions.get("video-input")?.y).toBe(positions.get("video-output")?.y);
expect(positions.get("image-input")?.y).toBeLessThan(positions.get("text")?.y ?? 0);
expect(positions.get("text")?.y).toBeLessThan(positions.get("video-input")?.y ?? 0);
}); });
}); });

136
src/utils/canvasAutoLayout.ts

@ -18,6 +18,16 @@ type LayoutNode = WorkflowNode & {
type: NonNullable<WorkflowNode["type"]>; type: NonNullable<WorkflowNode["type"]>;
}; };
interface LayoutSegment {
nodes: LayoutNode[];
edges: WorkflowEdge[];
}
interface SegmentLayout {
positions: CanvasAutoLayoutPosition[];
height: number;
}
function isLayoutNode(node: WorkflowNode): node is LayoutNode { function isLayoutNode(node: WorkflowNode): node is LayoutNode {
return Boolean(node.type) && node.type !== "group" && !node.parentId; return Boolean(node.type) && node.type !== "group" && !node.parentId;
} }
@ -118,29 +128,75 @@ function orderLayerNodes(
}); });
} }
export function calculateCanvasAutoLayout( function splitWorkflowSegments(nodes: LayoutNode[], edges: WorkflowEdge[]): LayoutSegment[] {
nodes: WorkflowNode[], const nodeById = new Map(nodes.map((node) => [node.id, node]));
edges: WorkflowEdge[], const adjacency = new Map(nodes.map((node) => [node.id, [] as string[]]));
options: CanvasAutoLayoutOptions = {}
): CanvasAutoLayoutPosition[] {
const layoutNodes = nodes.filter(isLayoutNode);
if (layoutNodes.length === 0) return [];
const columnGap = options.columnGap ?? DEFAULT_COLUMN_GAP; edges.forEach((edge) => {
const rowGap = options.rowGap ?? DEFAULT_ROW_GAP; if (!nodeById.has(edge.source) || !nodeById.has(edge.target)) return;
const layoutNodeIds = new Set(layoutNodes.map((node) => node.id)); adjacency.get(edge.source)?.push(edge.target);
const layoutEdges = edges.filter((edge) => { adjacency.get(edge.target)?.push(edge.source);
const edgeData = edge.data as { isLoop?: boolean } | undefined; });
return layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target) && !edgeData?.isLoop;
const visited = new Set<string>();
const segments: LayoutSegment[] = [];
nodes
.slice()
.sort(compareByCurrentPosition)
.forEach((startNode) => {
if (visited.has(startNode.id)) return;
const queue = [startNode.id];
const segmentNodeIds = new Set<string>();
while (queue.length > 0) {
const nodeId = queue.shift();
if (!nodeId || visited.has(nodeId)) continue;
visited.add(nodeId);
segmentNodeIds.add(nodeId);
(adjacency.get(nodeId) ?? []).forEach((neighborId) => {
if (!visited.has(neighborId)) queue.push(neighborId);
});
}
const segmentNodes = nodes
.filter((node) => segmentNodeIds.has(node.id))
.sort(compareByCurrentPosition);
const segmentEdges = edges.filter((edge) => segmentNodeIds.has(edge.source) && segmentNodeIds.has(edge.target));
segments.push({ nodes: segmentNodes, edges: segmentEdges });
});
return segments.sort((a, b) => {
const aTop = Math.min(...a.nodes.map((node) => node.position.y));
const bTop = Math.min(...b.nodes.map((node) => node.position.y));
if (aTop !== bTop) return aTop - bTop;
const aLeft = Math.min(...a.nodes.map((node) => node.position.x));
const bLeft = Math.min(...b.nodes.map((node) => node.position.x));
if (aLeft !== bLeft) return aLeft - bLeft;
return a.nodes[0]?.id.localeCompare(b.nodes[0]?.id ?? "") ?? 0;
}); });
}
function layoutSegment(
nodes: LayoutNode[],
edges: WorkflowEdge[],
startX: number,
startY: number,
columnGap: number,
rowGap: number
): SegmentLayout {
const positions: CanvasAutoLayoutPosition[] = []; const positions: CanvasAutoLayoutPosition[] = [];
const layerById = computeLayers(layoutNodes, layoutEdges); const layerById = computeLayers(nodes, edges);
const minX = Math.min(...layoutNodes.map((node) => node.position.x));
const minY = Math.min(...layoutNodes.map((node) => node.position.y));
const layers = new Map<number, LayoutNode[]>(); const layers = new Map<number, LayoutNode[]>();
const yById = new Map<string, number>(); const yById = new Map<string, number>();
layoutNodes.forEach((node) => { nodes.forEach((node) => {
const layer = layerById.get(node.id) ?? 0; const layer = layerById.get(node.id) ?? 0;
layers.set(layer, [...(layers.get(layer) ?? []), node]); layers.set(layer, [...(layers.get(layer) ?? []), node]);
}); });
@ -148,10 +204,10 @@ export function calculateCanvasAutoLayout(
const sortedLayers = Array.from(layers.keys()).sort((a, b) => a - b); const sortedLayers = Array.from(layers.keys()).sort((a, b) => a - b);
const orderedNodesByLayer = new Map<number, LayoutNode[]>(); const orderedNodesByLayer = new Map<number, LayoutNode[]>();
const layerXByLayer = new Map<number, number>(); const layerXByLayer = new Map<number, number>();
let currentX = minX; let currentX = startX;
sortedLayers.forEach((layer) => { sortedLayers.forEach((layer) => {
const layerNodes = orderLayerNodes(layers.get(layer) ?? [], layoutEdges, yById); const layerNodes = orderLayerNodes(layers.get(layer) ?? [], edges, yById);
orderedNodesByLayer.set(layer, layerNodes); orderedNodesByLayer.set(layer, layerNodes);
const maxLayerWidth = Math.max( const maxLayerWidth = Math.max(
DEFAULT_NODE_DIMENSIONS.width, DEFAULT_NODE_DIMENSIONS.width,
@ -163,7 +219,8 @@ export function calculateCanvasAutoLayout(
const maxRowCount = Math.max(0, ...Array.from(orderedNodesByLayer.values()).map((layerNodes) => layerNodes.length)); const maxRowCount = Math.max(0, ...Array.from(orderedNodesByLayer.values()).map((layerNodes) => layerNodes.length));
const rowYByIndex = new Map<number, number>(); const rowYByIndex = new Map<number, number>();
let currentY = minY; const rowHeightByIndex = new Map<number, number>();
let currentY = startY;
for (let rowIndex = 0; rowIndex < maxRowCount; rowIndex += 1) { for (let rowIndex = 0; rowIndex < maxRowCount; rowIndex += 1) {
rowYByIndex.set(rowIndex, currentY); rowYByIndex.set(rowIndex, currentY);
@ -173,6 +230,7 @@ export function calculateCanvasAutoLayout(
return node ? getNodeDimensions(node).height : 0; return node ? getNodeDimensions(node).height : 0;
}) })
); );
rowHeightByIndex.set(rowIndex, maxRowHeight);
currentY += maxRowHeight + rowGap; currentY += maxRowHeight + rowGap;
} }
@ -180,11 +238,11 @@ export function calculateCanvasAutoLayout(
const layerNodes = orderedNodesByLayer.get(layer) ?? []; const layerNodes = orderedNodesByLayer.get(layer) ?? [];
layerNodes.forEach((node, rowIndex) => { layerNodes.forEach((node, rowIndex) => {
const rowY = rowYByIndex.get(rowIndex) ?? minY; const rowY = rowYByIndex.get(rowIndex) ?? startY;
positions.push({ positions.push({
id: node.id, id: node.id,
position: { position: {
x: layerXByLayer.get(layer) ?? minX, x: layerXByLayer.get(layer) ?? startX,
y: rowY, y: rowY,
}, },
}); });
@ -194,5 +252,39 @@ export function calculateCanvasAutoLayout(
}); });
}); });
const height = Array.from(rowHeightByIndex.values()).reduce(
(sum, rowHeight, index) => sum + rowHeight + (index === rowHeightByIndex.size - 1 ? 0 : rowGap),
0
);
return { positions, height };
}
export function calculateCanvasAutoLayout(
nodes: WorkflowNode[],
edges: WorkflowEdge[],
options: CanvasAutoLayoutOptions = {}
): CanvasAutoLayoutPosition[] {
const layoutNodes = nodes.filter(isLayoutNode);
if (layoutNodes.length === 0) return [];
const columnGap = options.columnGap ?? DEFAULT_COLUMN_GAP;
const rowGap = options.rowGap ?? DEFAULT_ROW_GAP;
const layoutNodeIds = new Set(layoutNodes.map((node) => node.id));
const layoutEdges = edges.filter((edge) => {
const edgeData = edge.data as { isLoop?: boolean } | undefined;
return layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target) && !edgeData?.isLoop;
});
const positions: CanvasAutoLayoutPosition[] = [];
const minX = Math.min(...layoutNodes.map((node) => node.position.x));
const minY = Math.min(...layoutNodes.map((node) => node.position.y));
let currentY = minY;
splitWorkflowSegments(layoutNodes, layoutEdges).forEach((segment) => {
const segmentLayout = layoutSegment(segment.nodes, segment.edges, minX, currentY, columnGap, rowGap);
positions.push(...segmentLayout.positions);
currentY += segmentLayout.height + rowGap;
});
return positions; return positions;
} }

Loading…
Cancel
Save