Something went wrong. Try again.
ouija board1
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222import type { Bounds, EditorElement, ElementId } from "./index.js";
export type SelectionBoundsMode = "contains" | "intersects";
export type SelectionAlignment = | "left" | "center-x" | "right" | "top" | "center-y" | "bottom";
export type SelectionDistributionAxis = "x" | "y";
export interface SelectionBoundsChange { readonly id: ElementId; readonly bounds: Bounds;}
export interface SelectionPropertiesChange { readonly id: ElementId; readonly properties: Readonly<Record<string, unknown>>;}
function normalizedBounds(bounds: Bounds): Bounds { const right = bounds.x + bounds.width; const bottom = bounds.y + bounds.height; const x = Math.min(bounds.x, right); const y = Math.min(bounds.y, bottom); return { x, y, width: Math.abs(bounds.width), height: Math.abs(bounds.height), };}
function right(bounds: Bounds): number { return bounds.x + bounds.width;}
function bottom(bounds: Bounds): number { return bounds.y + bounds.height;}
export function selectionBounds( elements: readonly Pick<EditorElement, "bounds">[],): Bounds | null { if (elements.length === 0) { return null; }
let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; for (const element of elements) { const bounds = normalizedBounds(element.bounds); minX = Math.min(minX, bounds.x); minY = Math.min(minY, bounds.y); maxX = Math.max(maxX, right(bounds)); maxY = Math.max(maxY, bottom(bounds)); }
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, };}
export function selectElementsInBounds( elements: readonly EditorElement[], marquee: Bounds, mode: SelectionBoundsMode = "intersects",): readonly ElementId[] { const selection = normalizedBounds(marquee); const selectionRight = right(selection); const selectionBottom = bottom(selection);
return elements .filter((element) => { const bounds = normalizedBounds(element.bounds); if (mode === "contains") { return ( bounds.x >= selection.x && bounds.y >= selection.y && right(bounds) <= selectionRight && bottom(bounds) <= selectionBottom ); } return ( bounds.x < selectionRight && right(bounds) > selection.x && bounds.y < selectionBottom && bottom(bounds) > selection.y ); }) .map((element) => element.id);}
function alignedBounds( element: EditorElement, union: Bounds, alignment: SelectionAlignment,): Bounds { const bounds = normalizedBounds(element.bounds); let x = bounds.x; let y = bounds.y; if (alignment === "left") { x = union.x; } else if (alignment === "center-x") { x = union.x + (union.width - bounds.width) / 2; } else if (alignment === "right") { x = right(union) - bounds.width; } else if (alignment === "top") { y = union.y; } else if (alignment === "center-y") { y = union.y + (union.height - bounds.height) / 2; } else if (alignment === "bottom") { y = bottom(union) - bounds.height; } return { ...bounds, x, y };}
export function alignElements( elements: readonly EditorElement[], alignment: SelectionAlignment,): readonly SelectionBoundsChange[] { const union = selectionBounds(elements); if (union === null) { return []; } return elements.map((element) => ({ id: element.id, bounds: alignedBounds(element, union, alignment), }));}
export function distributeElements( elements: readonly EditorElement[], axis: SelectionDistributionAxis,): readonly SelectionBoundsChange[] { if (elements.length < 3) { return elements.map((element) => ({ id: element.id, bounds: normalizedBounds(element.bounds), })); }
const ordered = elements .map((element, index) => ({ element, index, bounds: normalizedBounds(element.bounds) })) .sort((first, second) => { const firstPosition = axis === "x" ? first.bounds.x : first.bounds.y; const secondPosition = axis === "x" ? second.bounds.x : second.bounds.y; return firstPosition - secondPosition || first.index - second.index; }); const first = ordered[0]!.bounds; const last = ordered[ordered.length - 1]!.bounds; const totalSize = ordered.reduce( (sum, entry) => sum + (axis === "x" ? entry.bounds.width : entry.bounds.height), 0, ); const outerSize = axis === "x" ? right(last) - first.x : bottom(last) - first.y; const gap = (outerSize - totalSize) / (ordered.length - 1); let cursor = axis === "x" ? first.x : first.y; const changes = new Map<ElementId, Bounds>();
for (const entry of ordered) { const bounds = entry.bounds; const nextBounds = axis === "x" ? { ...bounds, x: cursor } : { ...bounds, y: cursor }; changes.set(entry.element.id, nextBounds); cursor += (axis === "x" ? bounds.width : bounds.height) + gap; }
return elements.map((element) => ({ id: element.id, bounds: changes.get(element.id)!, }));}
export function isElementLocked( element: Pick<EditorElement, "properties">,): boolean { return element.properties.locked === true;}
export function setElementsLocked( elements: readonly EditorElement[], locked: boolean,): readonly SelectionPropertiesChange[] { return elements.map((element) => ({ id: element.id, properties: { ...element.properties, locked }, }));}
export function groupElements( elements: readonly EditorElement[], groupId: string,): readonly SelectionPropertiesChange[] { return elements.map((element) => ({ id: element.id, properties: { ...element.properties, groupId }, }));}
export function ungroupElements( elements: readonly EditorElement[],): readonly SelectionPropertiesChange[] { return elements.map((element) => { const properties = { ...element.properties }; delete properties.groupId; return { id: element.id, properties }; });}