Compare commits

...

2 Commits

Author SHA1 Message Date
dwelle
a050e87c04 fix type 2025-05-06 13:34:31 +02:00
dwelle
32da1819f9 feat: more idiomatic element filters [POC] 2025-05-06 13:11:45 +02:00
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
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,
[Attr] extends [never] ? OutputType : Attr
>
: Array<OutputType>;
// -----------------------------------------------------------------------------

View File

@ -26,6 +26,10 @@ import {
isTextElement,
} from "./typeChecks";
import { filterElements } from "./utils";
import { getFrameChildren } from "./frame";
import type Scene from "./Scene";
import type { Bounds } from "./bounds";
@ -65,23 +69,13 @@ export const dragSelectedElements = (
return true;
});
// we do not want a frame and its elements to be selected at the same time
// but when it happens (due to some bug), we want to avoid updating element
// in the frame twice, hence the use of set
const elementsToUpdate = new Set<NonDeletedExcalidrawElement>(
selectedElements,
// update frames and their children (use a set to make sure we avoid
// duplicates in case the user already selected the frame's children)
const elementsToUpdate = getFrameChildren(
scene.getNonDeletedElements(),
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[] = [];

View File

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

View File

@ -10,7 +10,7 @@ import {
type GlobalPoint,
} from "@excalidraw/math";
import { elementCenterPoint } from "@excalidraw/common";
import { elementCenterPoint, isReadonlyArray } from "@excalidraw/common";
import type { Curve, LineSegment } from "@excalidraw/math";
@ -18,8 +18,15 @@ import { getCornerRadius } from "./shapes";
import { getDiamondPoints } from "./bounds";
import type {
GenericAccumulator,
OutputAccumulator,
ReadonlyArrayOrMap,
} from "../../common/src/utility-types";
import type {
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawRectanguloidElement,
} from "./types";
@ -353,3 +360,35 @@ export function deconstructDiamondElement(
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
>;
};