import { AffineTransform, normalizeRect, Point, Rect, rectContainsPoint, rectsIntersect, Size, unionRects, } from "@noya-app/noya-geometry"; import { zip } from "@noya-app/noya-utils"; import { GetElementsOptions } from "../plugins/contextProperties"; export function filterElements( elements: CanvasElement[], options: GetElementsOptions ): CanvasElement[]; export function filterElements( elements: CanvasElement[], getRect: (element: CanvasElement) => Rect, options: GetElementsOptions ): CanvasElement[]; export function filterElements( ...args: | [CanvasElement[], GetElementsOptions] | [ CanvasElement[], (element: CanvasElement) => Rect, GetElementsOptions, ] ): CanvasElement[] { let [elements, getRect, options] = args.length === 2 ? [args[0], (element: CanvasElement) => element.rect, args[1]] : [args[0], args[1], args[2]]; elements = elements.toReversed(); if (options.predicate) { elements = elements.filter(options.predicate); } const elementsWithRects = zip(elements, elements.map(getRect)); if (options.at) { if ("width" in options.at) { const at = options.at; elements = elementsWithRects .filter(([_, rect]) => rectsIntersect(rect, at)) .map(([element]) => element); } else { const at = options.at; elements = elementsWithRects .filter(([_, rect]) => rectContainsPoint(rect, at)) .map(([element]) => element); } } return elements; } function normalizeSize(size: Size | number | false): Size { if (size === false) size = 0; if (typeof size === "number") size = { width: size, height: size }; return size; } interface Dimension { newValue: number; minSize: number; } /** * Updates a single dimension (width or height) with element and grid snapping */ function snapDimension({ newValue, minSize }: Dimension): number { return minSize === 0 ? newValue : Math.max(newValue, minSize); } /** * Calculates dimensions while maintaining aspect ratio */ function getDimensionsWithAspectRatio({ size, originalAspectRatio, minSize, }: { size: Size; originalAspectRatio: number; minSize: Size; }): Size { const newAspectRatio = size.width / size.height; let width = size.width; let height = size.height; // Adjust dimensions to maintain aspect ratio if (newAspectRatio > originalAspectRatio) { width = height * originalAspectRatio; } else { height = width / originalAspectRatio; } // Re-apply minimum size constraints if (width < minSize.width) { width = minSize.width; height = width / originalAspectRatio; } if (height < minSize.height) { height = minSize.height; width = height * originalAspectRatio; } return { width, height }; } type TransformDraftElementsOptions = { draftElements: CanvasElement[]; snapshotElements: CanvasElement[]; transform: AffineTransform; minimumElementSize?: Size | number | false; preserveAspectRatio?: boolean; normalizeNegativeScale?: boolean; }; type TransformableElementOptions = { updateElement: ( element: CanvasElement, newProps: { rect: Rect; flip: { x: boolean; y: boolean } } ) => void; getRect: (element: CanvasElement) => Rect; }; export function transformDraftElements( options: TransformDraftElementsOptions & TransformableElementOptions ): void; export function transformDraftElements< CanvasElement extends { id: string; rect: Rect }, >(options: TransformDraftElementsOptions): void; export function transformDraftElements({ draftElements, snapshotElements, transform, minimumElementSize = 1, preserveAspectRatio = false, normalizeNegativeScale = true, getRect = (element) => { if ("rect" in element) { return element.rect as Rect; } return { x: 0, y: 0, width: 0, height: 0 }; }, updateElement = (element, newProps) => { if ("rect" in element) { element.rect = newProps.rect; } }, }: TransformDraftElementsOptions & Partial>): void { const minSize = normalizeSize(minimumElementSize); // Get scale components to check for negative scaling const { x: scaleX, y: scaleY } = transform.scaleComponents; const hasNegativeScaleX = scaleX < 0; const hasNegativeScaleY = scaleY < 0; const hasNegativeScale = hasNegativeScaleX || hasNegativeScaleY; const isScaling = scaleX !== 1 || scaleY !== 1; const flip = { x: hasNegativeScaleX, y: hasNegativeScaleY }; if (hasNegativeScale && !normalizeNegativeScale) { // Prevent negative scaling by using absolute values const fixedTransform = AffineTransform.translate( transform.tx, transform.ty ).scale(Math.abs(scaleX), Math.abs(scaleY)); transform = fixedTransform; } draftElements.forEach((draftElement) => { const snapshotElement = snapshotElements.find( (snapshotElement) => snapshotElement.id === draftElement.id ); if (snapshotElement) { let resultRect = { ...getRect(snapshotElement) }; let newRect = transform.applyTo(getRect(snapshotElement)); // Normalize the rect if we have negative scaling if (hasNegativeScale && normalizeNegativeScale) { newRect = normalizeRect(newRect); } // Update position resultRect.x = newRect.x; resultRect.y = newRect.y; if (!isScaling) { updateElement(draftElement, { rect: resultRect, flip, }); return; } // Update dimensions let size: Size = { width: Math.max(newRect.width, minSize.width), height: Math.max(newRect.height, minSize.height), }; if (preserveAspectRatio) { // Use absolute values for aspect ratio calculation since we've already normalized const originalAspectRatio = Math.abs( getRect(snapshotElement).width / getRect(snapshotElement).height ); // Maintain aspect ratio while respecting minimum size size = getDimensionsWithAspectRatio({ size, originalAspectRatio, minSize, }); } else { // If not maintaining aspect ratio, snap dimensions independently size = { width: snapDimension({ newValue: size.width, minSize: minSize.width, }), height: snapDimension({ newValue: size.height, minSize: minSize.height, }), }; } resultRect.width = size.width; resultRect.height = size.height; updateElement(draftElement, { rect: resultRect, flip, }); } }); } export function getSelectionRect( elements: CanvasElement[] ): Rect | undefined; export function getSelectionRect( elements: CanvasElement[], getRect: (element: CanvasElement) => Rect ): Rect | undefined; export function getSelectionRect( ...args: | [CanvasElement[]] | [CanvasElement[], (element: CanvasElement) => Rect] ): Rect | undefined { const [elements, getRect] = args; if (elements.length === 0) { return undefined; } const rects = getRect ? elements.map(getRect) : elements.map((element) => element.rect); return unionRects(...rects.filter(Boolean)); } export function applyFlip

( point: P, flip: { x: boolean; y: boolean } ): P { return AffineTransform.scale(flip.x ? -1 : 1, flip.y ? -1 : 1, { x: 0.5, y: 0.5, }).applyTo(point); }