import { createBounds, Rect, unionRects } from "@noya-app/noya-geometry"; import { cartesianProduct, unique } from "@noya-app/noya-utils"; export type SnapLine = { x1: number; y1: number; x2: number; y2: number; }; function getValuesOnAxisForRect( rect: Rect, axis: "x" | "y" ): [number, number, number] { const bounds = createBounds(rect); return axis === "x" ? [bounds.minX, bounds.midX, bounds.maxX] : [bounds.minY, bounds.midY, bounds.maxY]; } export function getValuesOnAxis( rects: Rect | Rect[], axis: "x" | "y" ): number[] { if (!Array.isArray(rects)) { rects = [rects]; } return unique(rects.flatMap((rect) => getValuesOnAxisForRect(rect, axis))); } export function getElementSnapDelta({ movingValues, stationaryValues, proximity = 5, }: { movingValues: number[]; stationaryValues: number[]; proximity?: number; }): number | null { const pairs = cartesianProduct(movingValues, stationaryValues); const sortedPairs = pairs.sort(([a1, a2], [b1, b2]) => { const delta1 = Math.abs(a1 - a2); const delta2 = Math.abs(b1 - b2); return delta1 - delta2; }); const closestPair = sortedPairs.at(0); if (!closestPair) return null; const delta = closestPair[0] - closestPair[1]; if (Math.abs(delta) > proximity) return null; return -delta; } const EPSILON = 0.001; function isNearlyEqual(a: number, b: number, epsilon = EPSILON): boolean { return Math.abs(a - b) <= epsilon; } export function getSnapLines({ movingRect, stationaryRects, axis, proximity = 1, }: { movingRect: Rect; stationaryRects: Rect[]; axis: "x" | "y"; proximity?: number; }): SnapLine[] { const movingValues = getValuesOnAxis(movingRect, axis); const lines = movingValues.flatMap((value): SnapLine[] => { const relevantRects = stationaryRects.filter((rect) => { return getValuesOnAxis(rect, axis).some((v) => isNearlyEqual(v, value, proximity) ); }); if (relevantRects.length === 0) return []; const fullRect = unionRects(...relevantRects, movingRect); const bounds = createBounds(fullRect); if (axis === "x") { return [{ x1: value, y1: bounds.minY, x2: value, y2: bounds.maxY }]; } else { return [{ x1: bounds.minX, y1: value, x2: bounds.maxX, y2: value }]; } }); return lines; } export function getGridSnapDelta({ value, gridSize, }: { value: number; gridSize: number; }): number { const nearestValue = Math.round(value / gridSize) * gridSize; const delta = nearestValue - value; return delta; } export function getBestSnapDelta(snaps: (number | null)[]): number { const nonNullSnaps = snaps.filter((snap) => snap !== null); return nonNullSnaps.at(0) ?? 0; }