import type { CanvasGenerationDialogState, CanvasLayer, CanvasViewport, GenerateDialogState, } from './ImageCanvasEditorTypes'; type CanvasSize = { width: number; height: number }; type GenerationFrame = NonNullable; type Rect = { x: number; y: number; width: number; height: number }; const GENERATION_PLACEMENT_GAP = 32; const MAX_PLACEMENT_RING = 12; function getViewportWorldCenter({ canvasSize, viewport, }: { canvasSize: CanvasSize; viewport: CanvasViewport; }) { const safeScale = viewport.scale > 0 ? viewport.scale : 1; return { x: (canvasSize.width / 2 - viewport.x) / safeScale, y: (canvasSize.height / 2 - viewport.y) / safeScale, }; } function expandRect(rect: Rect, gap: number): Rect { return { x: rect.x - gap, y: rect.y - gap, width: rect.width + gap * 2, height: rect.height + gap * 2, }; } function rectsOverlap(a: Rect, b: Rect) { return ( a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y ); } function distanceSquared( a: { x: number; y: number }, b: { x: number; y: number }, ) { const dx = a.x - b.x; const dy = a.y - b.y; return dx * dx + dy * dy; } function buildBlockingRects({ layers, generationDialogs, }: { layers: CanvasLayer[]; generationDialogs: CanvasGenerationDialogState[]; }) { return [ ...layers .filter((layer) => !layer.hidden) .map((layer) => ({ x: layer.x, y: layer.y, width: layer.width, height: layer.height, })), ...generationDialogs.flatMap((dialog) => dialog.placeholder ? [ { x: dialog.placeholder.x, y: dialog.placeholder.y, width: dialog.placeholder.width, height: dialog.placeholder.height, }, ] : [], ), ].map((rect) => expandRect(rect, GENERATION_PLACEMENT_GAP)); } function isFree(candidate: Rect, blockingRects: Rect[]) { return !blockingRects.some((rect) => rectsOverlap(candidate, rect)); } function createCandidate({ center, frame, offsetX, offsetY, }: { center: { x: number; y: number }; frame: Pick; offsetX: number; offsetY: number; }): Rect { return { x: center.x - frame.width / 2 + offsetX, y: center.y - frame.height / 2 + offsetY, width: frame.width, height: frame.height, }; } function pushUniqueCandidate(candidates: Rect[], candidate: Rect) { if ( candidates.some( (current) => current.x === candidate.x && current.y === candidate.y, ) ) { return; } candidates.push(candidate); } function buildPlacementCandidates({ center, frame, blockingRects, }: { center: { x: number; y: number }; frame: Pick; blockingRects: Rect[]; }) { const baseX = center.x - frame.width / 2; const baseY = center.y - frame.height / 2; const stepX = frame.width + GENERATION_PLACEMENT_GAP; const stepY = frame.height + GENERATION_PLACEMENT_GAP; const candidates: Rect[] = []; pushUniqueCandidate(candidates, { x: baseX, y: baseY, width: frame.width, height: frame.height, }); blockingRects.forEach((rect) => { [ { x: rect.x + rect.width, y: baseY }, { x: baseX, y: rect.y + rect.height }, { x: rect.x - frame.width, y: baseY }, { x: baseX, y: rect.y - frame.height }, { x: rect.x + rect.width, y: rect.y + rect.height }, { x: rect.x - frame.width, y: rect.y + rect.height }, { x: rect.x + rect.width, y: rect.y - frame.height }, { x: rect.x - frame.width, y: rect.y - frame.height }, ].forEach((candidate) => pushUniqueCandidate(candidates, { ...candidate, width: frame.width, height: frame.height, }), ); }); // 组合不同阻挡物的水平 / 垂直边界,覆盖中心被多块图层夹住时的最近空位。 const edgeXs = new Set([baseX]); const edgeYs = new Set([baseY]); blockingRects.forEach((rect) => { edgeXs.add(rect.x + rect.width); edgeXs.add(rect.x - frame.width); edgeYs.add(rect.y + rect.height); edgeYs.add(rect.y - frame.height); }); edgeXs.forEach((x) => { edgeYs.forEach((y) => { pushUniqueCandidate(candidates, { x, y, width: frame.width, height: frame.height, }); }); }); for (let ring = 1; ring <= MAX_PLACEMENT_RING; ring += 1) { const offsets = [ { x: ring, y: 0 }, { x: 0, y: ring }, { x: -ring, y: 0 }, { x: 0, y: -ring }, { x: ring, y: ring }, { x: -ring, y: ring }, { x: ring, y: -ring }, { x: -ring, y: -ring }, ]; offsets.forEach((offset) => pushUniqueCandidate( candidates, createCandidate({ center, frame, offsetX: offset.x * stepX, offsetY: offset.y * stepY, }), ), ); } return candidates.sort((a, b) => { const aDistance = distanceSquared( { x: a.x + a.width / 2, y: a.y + a.height / 2 }, center, ); const bDistance = distanceSquared( { x: b.x + b.width / 2, y: b.y + b.height / 2 }, center, ); if (aDistance !== bDistance) { return aDistance - bDistance; } const aDownBias = a.y >= baseY ? 0 : 1; const bDownBias = b.y >= baseY ? 0 : 1; if (aDownBias !== bDownBias) { return aDownBias - bDownBias; } if (a.x !== b.x) { return a.x - b.x; } return a.y - b.y; }); } export function chooseGenerationPlacement({ canvasSize, viewport, frame, layers, generationDialogs, }: { canvasSize: CanvasSize; viewport: CanvasViewport; frame: GenerationFrame; layers: CanvasLayer[]; generationDialogs: CanvasGenerationDialogState[]; }): GenerationFrame { const center = getViewportWorldCenter({ canvasSize, viewport }); const blockingRects = buildBlockingRects({ layers, generationDialogs }); const candidates = buildPlacementCandidates({ center, frame, blockingRects }); const fallbackPlacement = createCandidate({ center, frame, offsetX: 0, offsetY: 0, }); const placement = candidates.find((candidate) => isFree(candidate, blockingRects)) ?? fallbackPlacement; return { ...frame, x: placement.x, y: placement.y, }; } export function centerViewportOnPlacement({ canvasSize, viewport, placement, }: { canvasSize: CanvasSize; viewport: CanvasViewport; placement: GenerationFrame; }): CanvasViewport { const scale = viewport.scale > 0 ? viewport.scale : 1; return { x: canvasSize.width / 2 - (placement.x + placement.width / 2) * scale, y: canvasSize.height / 2 - (placement.y + placement.height / 2) * scale, scale, }; }