Compare commits

...

4 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
Narek Malkhasyan cec5232a7a fix: when resizing element, update bound elements after final size of element is determined (#9475) 2025-05-05 12:15:42 +02:00
Márk Tolmács d4f70e9f31 feat: Quarter snap points for diamonds (#9387) 2025-05-05 11:34:40 +02:00
6 changed files with 154 additions and 33 deletions
+25
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>;
// -----------------------------------------------------------------------------
+42
View File
@@ -1171,6 +1171,48 @@ export const snapToMid = (
center,
angle,
);
} else if (element.type === "diamond") {
const distance = FIXED_BINDING_DISTANCE - 1;
const topLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + height / 4 - distance,
);
const topRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + height / 4 - distance,
);
const bottomLeft = pointFrom<GlobalPoint>(
x + width / 4 - distance,
y + (3 * height) / 4 + distance,
);
const bottomRight = pointFrom<GlobalPoint>(
x + (3 * width) / 4 + distance,
y + (3 * height) / 4 + distance,
);
if (
pointDistance(topLeft, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(topLeft, center, angle);
}
if (
pointDistance(topRight, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(topRight, center, angle);
}
if (
pointDistance(bottomLeft, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(bottomLeft, center, angle);
}
if (
pointDistance(bottomRight, nonRotated) <
Math.max(horizontalThrehsold, verticalThrehsold)
) {
return pointRotateRads(bottomRight, center, angle);
}
}
return p;
+10 -16
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[] = [];
+32 -11
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 = (
+5 -5
View File
@@ -962,11 +962,6 @@ export const resizeSingleElement = (
isDragging: false,
});
updateBoundElements(latestElement, scene, {
// TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight },
});
if (boundTextElement && boundTextFont != null) {
scene.mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
@@ -978,6 +973,11 @@ export const resizeSingleElement = (
handleDirection,
shouldMaintainAspectRatio,
);
updateBoundElements(latestElement, scene, {
// TODO: confirm with MARK if this actually makes sense
newSize: { width: nextWidth, height: nextHeight },
});
}
};
+40 -1
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
>;
};