* Refactor: simplify linear element type * Refactor: dedupe scrollbar handling * First step towards binding - establish relationship and basic test for dragged lines * Refactor: use zoom from appstate * Refactor: generalize getElementAtPosition * Only consider bindable elements in hit test * Refactor: pull out pieces of hit test for reuse later * Refactor: pull out diamond from hit test for reuse later * Refactor: pull out text from hit test for reuse later * Suggest binding when hovering * Give shapes in regression test real size * Give shapes in undo/redo test real size * Keep bound element highlighted * Show binding suggestion for multi-point elements * Move binding to its on module with functions so that I can use it from actions, add support for binding end of multi-point elements * Use Id instead of ID * Improve boundary offset for non-squarish elements * Fix localStorage for binding on linear elements * Simplify dragging code and fix elements bound twice to the same shape * Fix binding for rectangles * Bind both ends at the end of the linear element creation, needed for focus points * wip * Refactor: Renames and reshapes for next commit * Calculate and store focus points and gaps, but dont use them yet * Focus points for rectangles * Dont blow up when canceling linear element * Stop suggesting binding when a non-compatible tool is selected * Clean up collision code * Using Geometric Algebra for hit tests * Correct binding for all shapes * Constant gap around polygon corners * Fix rotation handling * Generalize update and fix hit test for rotated elements * Handle rotation realtime * Handle scaling * Remove vibration when moving bound and binding element together * Handle simultenous scaling * Allow binding and unbinding when editing linear elements * Dont delete binding when the end point wasnt touched * Bind on enter/escape when editing * Support multiple suggested bindable elements in preparation for supporting linear elements dragging * Update binding when moving linear elements * Update binding when resizing linear elements * Dont re-render UI on binding hints * Update both ends when one is moved * Use distance instead of focus point for binding * Complicated approach for posterity, ignore this commit * Revert the complicated approach * Better focus point strategy, working for all shapes * Update snapshots * Dont break binding gap when mirroring shape * Dont break binding gap when grid mode pushes it inside * Dont bind draw elements * Support alt duplication * Fix alt duplication to * Support cmd+D duplication * All copy mechanisms are supported * Allow binding shapes to arrows, having arrows created first * Prevent arrows from disappearing for ellipses * Better binding suggestion highlight for shapes * Dont suggest second binding for simple elements when editing or moving them * Dont steal already bound linear elements when moving shapes * Fix highlighting diamonds and more precisely highlight other shapes * Highlight linear element edges for binding * Highlight text binding too * Handle deletion * Dont suggest second binding for simple linear elements when creating them * Dont highlight bound element during creation * Fix binding for rotated linear elements * Fix collision check for ellipses * Dont show suggested bindings for selected pairs * Bind multi-point linear elements when the tool is switched - important for mobile * Handle unbinding one of two bound edges correctly * Rename boundElement in state to startBoundElement * Dont double account for zoom when rendering binding highlight * Fix rendering of edited linear element point handles * Suggest binding when adding new point to a linear element * Bind when adding a new point to a linear element and dont unbind when moving middle elements * Handle deleting points * Add cmd modifier key to disable binding * Use state for enabling binding, fix not binding for linear elements during creation * Drop support for binding lines, only arrows are bindable * Reset binding mode on blur * Fix not binding lines
246 lines
5.8 KiB
TypeScript
246 lines
5.8 KiB
TypeScript
import { ExcalidrawElement, PointerType } from "./types";
|
|
|
|
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
|
import { rotate } from "../math";
|
|
|
|
type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se" | "rotation";
|
|
|
|
export type Handlers = Partial<
|
|
{ [T in Sides]: [number, number, number, number] }
|
|
>;
|
|
|
|
const handleSizes: { [k in PointerType]: number } = {
|
|
mouse: 8,
|
|
pen: 16,
|
|
touch: 28,
|
|
};
|
|
|
|
const ROTATION_HANDLER_GAP = 16;
|
|
|
|
export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
|
|
e: true,
|
|
s: true,
|
|
n: true,
|
|
w: true,
|
|
};
|
|
|
|
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
|
|
e: true,
|
|
s: true,
|
|
n: true,
|
|
w: true,
|
|
};
|
|
|
|
const OMIT_SIDES_FOR_LINE_SLASH = {
|
|
e: true,
|
|
s: true,
|
|
n: true,
|
|
w: true,
|
|
nw: true,
|
|
se: true,
|
|
rotation: true,
|
|
};
|
|
|
|
const OMIT_SIDES_FOR_LINE_BACKSLASH = {
|
|
e: true,
|
|
s: true,
|
|
n: true,
|
|
w: true,
|
|
ne: true,
|
|
sw: true,
|
|
rotation: true,
|
|
};
|
|
|
|
const generateHandler = (
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
cx: number,
|
|
cy: number,
|
|
angle: number,
|
|
): [number, number, number, number] => {
|
|
const [xx, yy] = rotate(x + width / 2, y + height / 2, cx, cy, angle);
|
|
return [xx - width / 2, yy - height / 2, width, height];
|
|
};
|
|
|
|
export const handlerRectanglesFromCoords = (
|
|
[x1, y1, x2, y2]: Bounds,
|
|
angle: number,
|
|
zoom: number,
|
|
pointerType: PointerType = "mouse",
|
|
omitSides: { [T in Sides]?: boolean } = {},
|
|
): Handlers => {
|
|
const size = handleSizes[pointerType];
|
|
const handlerWidth = size / zoom;
|
|
const handlerHeight = size / zoom;
|
|
|
|
const handlerMarginX = size / zoom;
|
|
const handlerMarginY = size / zoom;
|
|
|
|
const width = x2 - x1;
|
|
const height = y2 - y1;
|
|
const cx = (x1 + x2) / 2;
|
|
const cy = (y1 + y2) / 2;
|
|
|
|
const dashedLineMargin = 4 / zoom;
|
|
|
|
const centeringOffset = (size - 8) / (2 * zoom);
|
|
|
|
const handlers: Partial<
|
|
{ [T in Sides]: [number, number, number, number] }
|
|
> = {
|
|
nw: omitSides["nw"]
|
|
? undefined
|
|
: generateHandler(
|
|
x1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
|
y1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
),
|
|
ne: omitSides["ne"]
|
|
? undefined
|
|
: generateHandler(
|
|
x2 + dashedLineMargin - centeringOffset,
|
|
y1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
),
|
|
sw: omitSides["sw"]
|
|
? undefined
|
|
: generateHandler(
|
|
x1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
|
y2 + dashedLineMargin - centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
),
|
|
se: omitSides["se"]
|
|
? undefined
|
|
: generateHandler(
|
|
x2 + dashedLineMargin - centeringOffset,
|
|
y2 + dashedLineMargin - centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
),
|
|
rotation: omitSides["rotation"]
|
|
? undefined
|
|
: generateHandler(
|
|
x1 + width / 2 - handlerWidth / 2,
|
|
y1 -
|
|
dashedLineMargin -
|
|
handlerMarginY +
|
|
centeringOffset -
|
|
ROTATION_HANDLER_GAP / zoom,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
),
|
|
};
|
|
|
|
// We only want to show height handlers (all cardinal directions) above a certain size
|
|
const minimumSizeForEightHandlers = (5 * size) / zoom;
|
|
if (Math.abs(width) > minimumSizeForEightHandlers) {
|
|
if (!omitSides["n"]) {
|
|
handlers["n"] = generateHandler(
|
|
x1 + width / 2 - handlerWidth / 2,
|
|
y1 - dashedLineMargin - handlerMarginY + centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
);
|
|
}
|
|
if (!omitSides["s"]) {
|
|
handlers["s"] = generateHandler(
|
|
x1 + width / 2 - handlerWidth / 2,
|
|
y2 + dashedLineMargin - centeringOffset,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
);
|
|
}
|
|
}
|
|
if (Math.abs(height) > minimumSizeForEightHandlers) {
|
|
if (!omitSides["w"]) {
|
|
handlers["w"] = generateHandler(
|
|
x1 - dashedLineMargin - handlerMarginX + centeringOffset,
|
|
y1 + height / 2 - handlerHeight / 2,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
);
|
|
}
|
|
if (!omitSides["e"]) {
|
|
handlers["e"] = generateHandler(
|
|
x2 + dashedLineMargin - centeringOffset,
|
|
y1 + height / 2 - handlerHeight / 2,
|
|
handlerWidth,
|
|
handlerHeight,
|
|
cx,
|
|
cy,
|
|
angle,
|
|
);
|
|
}
|
|
}
|
|
|
|
return handlers;
|
|
};
|
|
|
|
export const handlerRectangles = (
|
|
element: ExcalidrawElement,
|
|
zoom: number,
|
|
pointerType: PointerType = "mouse",
|
|
) => {
|
|
let omitSides: { [T in Sides]?: boolean } = {};
|
|
if (
|
|
element.type === "arrow" ||
|
|
element.type === "line" ||
|
|
element.type === "draw"
|
|
) {
|
|
if (element.points.length === 2) {
|
|
// only check the last point because starting point is always (0,0)
|
|
const [, p1] = element.points;
|
|
if (p1[0] === 0 || p1[1] === 0) {
|
|
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
|
} else if (p1[0] > 0 && p1[1] < 0) {
|
|
omitSides = OMIT_SIDES_FOR_LINE_SLASH;
|
|
} else if (p1[0] > 0 && p1[1] > 0) {
|
|
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
|
} else if (p1[0] < 0 && p1[1] > 0) {
|
|
omitSides = OMIT_SIDES_FOR_LINE_SLASH;
|
|
} else if (p1[0] < 0 && p1[1] < 0) {
|
|
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
|
}
|
|
}
|
|
} else if (element.type === "text") {
|
|
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
|
}
|
|
|
|
return handlerRectanglesFromCoords(
|
|
getElementAbsoluteCoords(element),
|
|
element.angle,
|
|
zoom,
|
|
pointerType,
|
|
omitSides,
|
|
);
|
|
};
|