feat: more idiomatic element filters [POC]

This commit is contained in:
dwelle 2025-05-06 13:11:45 +02:00
parent cec5232a7a
commit 32da1819f9
4 changed files with 107 additions and 28 deletions

View File

@ -68,3 +68,28 @@ export type MaybePromise<T> = T | Promise<T>;
// get union of all keys from the union of types // get union of all keys from the union of types
export type AllPossibleKeys<T> = T extends any ? keyof T : never; export type AllPossibleKeys<T> = T extends any ? keyof T : never;
// utlity types for filter helper and related data structures
// -----------------------------------------------------------------------------
export type ReadonlyArrayOrMap<
T,
K = T extends { id: string } ? T["id"] : string,
> = readonly T[] | ReadonlyMap<K, T>;
export type GenericAccumulator<T = unknown> = Set<T> | Map<T, T> | Array<T>;
export type ArrayAccumulator<T = unknown> = Array<T>;
export type MapAccumulator<T = unknown, K = unknown> = Map<T, K>;
export type SetAccumulator<T = unknown> = Set<T>;
export type OutputAccumulator<
Accumulator,
OutputType,
Attr extends keyof OutputType = never,
> = Accumulator extends SetAccumulator
? Set<[Attr] extends [never] ? OutputType : Attr>
: Accumulator extends MapAccumulator
? Map<
OutputType extends { id: string } ? OutputType["id"] : string,
OutputType
>
: Array<OutputType>;
// -----------------------------------------------------------------------------

View File

@ -26,6 +26,10 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { filterElements } from "./utils";
import { getFrameChildren } from "./frame";
import type Scene from "./Scene"; import type Scene from "./Scene";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
@ -65,23 +69,13 @@ export const dragSelectedElements = (
return true; return true;
}); });
// we do not want a frame and its elements to be selected at the same time // update frames and their children (use a set to make sure we avoid
// but when it happens (due to some bug), we want to avoid updating element // duplicates in case the user already selected the frame's children)
// in the frame twice, hence the use of set const elementsToUpdate = getFrameChildren(
const elementsToUpdate = new Set<NonDeletedExcalidrawElement>( scene.getNonDeletedElements(),
selectedElements, filterElements(selectedElements, isFrameLikeElement, new Set(), "id"),
new Set(selectedElements),
); );
const frames = selectedElements
.filter((e) => isFrameLikeElement(e))
.map((f) => f.id);
if (frames.length > 0) {
for (const element of scene.getNonDeletedElements()) {
if (element.frameId !== null && frames.includes(element.frameId)) {
elementsToUpdate.add(element);
}
}
}
const origElements: ExcalidrawElement[] = []; const origElements: ExcalidrawElement[] = [];

View File

@ -9,7 +9,13 @@ import type {
StaticCanvasAppState, StaticCanvasAppState,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type { ReadonlySetLike } from "@excalidraw/common/utility-types"; import type {
ArrayAccumulator,
GenericAccumulator,
ReadonlyArrayOrMap,
ReadonlySetLike,
OutputAccumulator,
} from "@excalidraw/common/utility-types";
import { getElementsWithinSelection, getSelectedElements } from "./selection"; import { getElementsWithinSelection, getSelectedElements } from "./selection";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
@ -27,6 +33,8 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { filterElements } from "./utils";
import type { ExcalidrawElementsIncludingDeleted } from "./Scene"; import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
import type { import type {
@ -230,17 +238,30 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => {
return frameElementsMap; return frameElementsMap;
}; };
export const getFrameChildren = ( export const getFrameChildren = <
allElements: ElementsMapOrArray, K extends ExcalidrawElement,
frameId: string, O extends GenericAccumulator = ArrayAccumulator,
) => { >(
const frameChildren: ExcalidrawElement[] = []; allElements: ReadonlyArrayOrMap<K>,
for (const element of allElements.values()) { frameId: K["id"] | Set<string>,
if (element.frameId === frameId) { output?: O,
frameChildren.push(element); ): OutputAccumulator<O, K> => {
if (frameId instanceof Set && frameId.size === 0) {
return (output || []) as any as OutputAccumulator<O, K>;
} }
return filterElements(
allElements,
(element): element is K => {
if (!element.frameId) {
return false;
} }
return frameChildren; return typeof frameId === "string"
? element.frameId === frameId
: frameId.has(element.frameId);
},
output || [],
) as any as OutputAccumulator<O, K>;
}; };
export const getFrameLikeElements = ( export const getFrameLikeElements = (

View File

@ -10,7 +10,7 @@ import {
type GlobalPoint, type GlobalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { elementCenterPoint } from "@excalidraw/common"; import { elementCenterPoint, isReadonlyArray } from "@excalidraw/common";
import type { Curve, LineSegment } from "@excalidraw/math"; import type { Curve, LineSegment } from "@excalidraw/math";
@ -18,8 +18,15 @@ import { getCornerRadius } from "./shapes";
import { getDiamondPoints } from "./bounds"; import { getDiamondPoints } from "./bounds";
import type {
GenericAccumulator,
OutputAccumulator,
ReadonlyArrayOrMap,
} from "../../common/src/utility-types";
import type { import type {
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawRectanguloidElement, ExcalidrawRectanguloidElement,
} from "./types"; } from "./types";
@ -353,3 +360,35 @@ export function deconstructDiamondElement(
return [sides, corners]; return [sides, corners];
} }
export const filterElements = <
InputType extends ExcalidrawElement,
PredicateOutputType extends InputType,
AccumulatorType extends GenericAccumulator,
Attr extends keyof PredicateOutputType = never,
>(
elements: ReadonlyArrayOrMap<InputType>,
predicate: (elem: InputType) => elem is PredicateOutputType,
accumulator: AccumulatorType,
attr?: Attr,
): OutputAccumulator<AccumulatorType, PredicateOutputType, Attr> => {
for (const element of isReadonlyArray(elements)
? elements
: elements.values()) {
if (predicate(element)) {
if (accumulator instanceof Set) {
accumulator.add(attr ? element[attr] : element);
} else if (accumulator instanceof Map) {
accumulator.set(element.id, attr ? element[attr] : element);
} else {
accumulator.push(element);
}
}
}
return accumulator as any as OutputAccumulator<
AccumulatorType,
PredicateOutputType,
Attr
>;
};