mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
refactor: update collision from ga to vector geometry (#7636)
* new collision api * isPointOnShape * removed redundant code * new collision methods in app * curve shape takes starting point * clean up geometry * curve rotation * freedraw * inside curve * improve ellipse inside check * ellipse distance func * curve inside * include frame name bounds * replace previous private methods for getting elements at x,y * arrow bound text hit detection * keep iframes on top * remove dependence on old collision methods from app * remove old collision functions * move some hit functions outside of app * code refactor * type * text collision from inside * fix context menu test * highest z-index collision * fix 1px away binding test * strictly less * remove unused imports * lint * 'ignore' resize flipping test * more lint fix * skip 'flips while resizing' test * more test * fix merge errors * fix selection in resize test * added a bit more comment --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
3e334a67ed
commit
bbdcd30a73
20 changed files with 2721 additions and 1627 deletions
|
@ -8,7 +8,6 @@ import { register } from "./register";
|
|||
import { mutateElement } from "../element/mutateElement";
|
||||
import { isPathALoop } from "../math";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
maybeBindLinearElement,
|
||||
bindOrUnbindLinearElement,
|
||||
|
@ -21,12 +20,9 @@ export const actionFinalize = register({
|
|||
name: "finalize",
|
||||
label: "",
|
||||
trackEvent: false,
|
||||
perform: (
|
||||
elements,
|
||||
appState,
|
||||
_,
|
||||
{ interactiveCanvas, focusContainer, scene },
|
||||
) => {
|
||||
perform: (elements, appState, _, app) => {
|
||||
const { interactiveCanvas, focusContainer, scene } = app;
|
||||
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
if (appState.editingLinearElement) {
|
||||
|
@ -131,13 +127,7 @@ export const actionFinalize = register({
|
|||
-1,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
maybeBindLinearElement(
|
||||
multiPointElement,
|
||||
appState,
|
||||
Scene.getScene(multiPointElement)!,
|
||||
{ x, y },
|
||||
elementsMap,
|
||||
);
|
||||
maybeBindLinearElement(multiPointElement, appState, { x, y }, app);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { resizeMultipleElements } from "../element/resizeElements";
|
||||
import { AppState } from "../types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getCommonBoundingBox } from "../element/bounds";
|
||||
|
@ -32,6 +32,7 @@ export const actionFlipHorizontal = register({
|
|||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"horizontal",
|
||||
app,
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
|
@ -56,6 +57,7 @@ export const actionFlipVertical = register({
|
|||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"vertical",
|
||||
app,
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
|
@ -73,6 +75,7 @@ const flipSelectedElements = (
|
|||
elementsMap: NonDeletedSceneElementsMap,
|
||||
appState: Readonly<AppState>,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
|
@ -89,6 +92,7 @@ const flipSelectedElements = (
|
|||
elementsMap,
|
||||
appState,
|
||||
flipDirection,
|
||||
app,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
@ -104,6 +108,7 @@ const flipElements = (
|
|||
elementsMap: NonDeletedSceneElementsMap,
|
||||
appState: AppState,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
app: AppClassProperties,
|
||||
): ExcalidrawElement[] => {
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||
|
||||
|
@ -118,7 +123,7 @@ const flipElements = (
|
|||
);
|
||||
|
||||
isBindingEnabled(appState)
|
||||
? bindOrUnbindSelectedElements(selectedElements, elements, elementsMap)
|
||||
? bindOrUnbindSelectedElements(selectedElements, app)
|
||||
: unbindLinearElements(selectedElements, elementsMap);
|
||||
|
||||
return selectedElements;
|
||||
|
|
|
@ -107,8 +107,6 @@ import {
|
|||
getResizeOffsetXY,
|
||||
getLockedLinearCursorAlignSize,
|
||||
getTransformHandleTypeFromCoords,
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
isInvisiblySmallElement,
|
||||
isNonDeletedElement,
|
||||
isTextElement,
|
||||
|
@ -119,6 +117,7 @@ import {
|
|||
transformElements,
|
||||
updateTextElement,
|
||||
redrawTextBoundingBox,
|
||||
getElementAbsoluteCoords,
|
||||
} from "../element";
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
|
@ -162,6 +161,7 @@ import {
|
|||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
isMagicFrameElement,
|
||||
isTextBindableContainer,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawBindableElement,
|
||||
|
@ -212,7 +212,6 @@ import {
|
|||
} from "../math";
|
||||
import {
|
||||
calculateScrollCenter,
|
||||
getElementsAtPosition,
|
||||
getElementsWithinSelection,
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
|
@ -223,6 +222,15 @@ import Scene from "../scene/Scene";
|
|||
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
GeometricShape,
|
||||
getClosedCurveShape,
|
||||
getCurveShape,
|
||||
getEllipseShape,
|
||||
getFreedrawShape,
|
||||
getPolygonShape,
|
||||
} from "../../utils/geometry/shape";
|
||||
import { isPointInShape } from "../../utils/collision";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
|
@ -318,11 +326,9 @@ import {
|
|||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
getLineHeightInPx,
|
||||
getTextBindableContainerAtPosition,
|
||||
isMeasureTextSupported,
|
||||
isValidTextContainer,
|
||||
} from "../element/textElement";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||
import {
|
||||
showHyperlinkTooltip,
|
||||
hideHyperlinkToolip,
|
||||
|
@ -407,6 +413,13 @@ import { AnimatedTrail } from "../animated-trail";
|
|||
import { LaserTrails } from "../laser-trails";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { getRenderOpacity } from "../renderer/renderElement";
|
||||
import {
|
||||
hitElementBoundText,
|
||||
hitElementBoundingBox,
|
||||
hitElementBoundingBoxOnly,
|
||||
hitElementItself,
|
||||
shouldTestInside,
|
||||
} from "../element/collision";
|
||||
import { textWysiwyg } from "../element/textWysiwyg";
|
||||
import { isOverScrollBars } from "../scene/scrollbars";
|
||||
import {
|
||||
|
@ -2757,7 +2770,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
maybeBindLinearElement(
|
||||
multiElement,
|
||||
this.state,
|
||||
this.scene,
|
||||
tupleToCoors(
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
multiElement,
|
||||
|
@ -2765,7 +2777,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
elementsMap,
|
||||
),
|
||||
),
|
||||
elementsMap,
|
||||
this,
|
||||
);
|
||||
}
|
||||
this.history.record(this.state, elements);
|
||||
|
@ -4048,11 +4060,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
isBindingEnabled(this.state)
|
||||
? bindOrUnbindSelectedElements(
|
||||
selectedElements,
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
)
|
||||
? bindOrUnbindSelectedElements(selectedElements, this)
|
||||
: unbindLinearElements(selectedElements, elementsMap);
|
||||
this.setState({ suggestedBindings: [] });
|
||||
}
|
||||
|
@ -4355,12 +4363,87 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the pure geometric shape of an excalidraw element
|
||||
* which is then used for hit detection
|
||||
*/
|
||||
public getElementShape(element: ExcalidrawElement): GeometricShape {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
case "embeddable":
|
||||
case "image":
|
||||
case "iframe":
|
||||
case "text":
|
||||
case "selection":
|
||||
return getPolygonShape(element);
|
||||
case "arrow":
|
||||
case "line": {
|
||||
const roughShape =
|
||||
ShapeCache.get(element)?.[0] ??
|
||||
ShapeCache.generateElementShape(element, null)[0];
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
return shouldTestInside(element)
|
||||
? getClosedCurveShape(
|
||||
roughShape,
|
||||
[element.x, element.y],
|
||||
element.angle,
|
||||
[cx, cy],
|
||||
)
|
||||
: getCurveShape(roughShape, [element.x, element.y], element.angle, [
|
||||
cx,
|
||||
cy,
|
||||
]);
|
||||
}
|
||||
|
||||
case "ellipse":
|
||||
return getEllipseShape(element);
|
||||
|
||||
case "freedraw": {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
return getFreedrawShape(element, [cx, cy], shouldTestInside(element));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getBoundTextShape(element: ExcalidrawElement): GeometricShape | null {
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (boundTextElement) {
|
||||
if (element.type === "arrow") {
|
||||
return this.getElementShape({
|
||||
...boundTextElement,
|
||||
// arrow's bound text accurate position is not stored in the element's property
|
||||
// but rather calculated and returned from the following static method
|
||||
...LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
return this.getElementShape(boundTextElement);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getElementAtPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
opts?: {
|
||||
/** if true, returns the first selected element (with highest z-index)
|
||||
of all hit elements */
|
||||
preferSelected?: boolean;
|
||||
includeBoundTextElement?: boolean;
|
||||
includeLockedElements?: boolean;
|
||||
|
@ -4372,6 +4455,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
opts?.includeBoundTextElement,
|
||||
opts?.includeLockedElements,
|
||||
);
|
||||
|
||||
if (allHitElements.length > 1) {
|
||||
if (opts?.preferSelected) {
|
||||
for (let index = allHitElements.length - 1; index > -1; index--) {
|
||||
|
@ -4382,22 +4466,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
const elementWithHighestZIndex =
|
||||
allHitElements[allHitElements.length - 1];
|
||||
|
||||
// If we're hitting element with highest z-index only on its bounding box
|
||||
// while also hitting other element figure, the latter should be considered.
|
||||
return isHittingElementBoundingBoxWithoutHittingElement(
|
||||
elementWithHighestZIndex,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
return isPointInShape(
|
||||
[x, y],
|
||||
this.getElementShape(elementWithHighestZIndex),
|
||||
)
|
||||
? allHitElements[allHitElements.length - 2]
|
||||
: elementWithHighestZIndex;
|
||||
? elementWithHighestZIndex
|
||||
: allHitElements[allHitElements.length - 2];
|
||||
}
|
||||
if (allHitElements.length === 1) {
|
||||
return allHitElements[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -4407,7 +4489,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
includeBoundTextElement: boolean = false,
|
||||
includeLockedElements: boolean = false,
|
||||
): NonDeleted<ExcalidrawElement>[] {
|
||||
const elements =
|
||||
const iframeLikes: ExcalidrawIframeElement[] = [];
|
||||
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
const elements = (
|
||||
includeBoundTextElement && includeLockedElements
|
||||
? this.scene.getNonDeletedElements()
|
||||
: this.scene
|
||||
|
@ -4417,29 +4503,120 @@ class App extends React.Component<AppProps, AppState> {
|
|||
(includeLockedElements || !element.locked) &&
|
||||
(includeBoundTextElement ||
|
||||
!(isTextElement(element) && element.containerId)),
|
||||
);
|
||||
)
|
||||
)
|
||||
.filter((el) => this.hitElement(x, y, el))
|
||||
.filter((element) => {
|
||||
// hitting a frame's element from outside the frame is not considered a hit
|
||||
const containingFrame = getContainingFrame(element, elementsMap);
|
||||
return containingFrame &&
|
||||
this.state.frameRendering.enabled &&
|
||||
this.state.frameRendering.clip
|
||||
? isCursorInFrame({ x, y }, containingFrame, elementsMap)
|
||||
: true;
|
||||
})
|
||||
.filter((el) => {
|
||||
// The parameter elements comes ordered from lower z-index to higher.
|
||||
// We want to preserve that order on the returned array.
|
||||
// Exception being embeddables which should be on top of everything else in
|
||||
// terms of hit testing.
|
||||
if (isIframeElement(el)) {
|
||||
iframeLikes.push(el);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.concat(iframeLikes) as NonDeleted<ExcalidrawElement>[];
|
||||
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
return getElementsAtPosition(elements, (element) =>
|
||||
hitTest(
|
||||
element,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
return elements;
|
||||
}
|
||||
|
||||
private getHitThreshold() {
|
||||
return 10 / this.state.zoom.value;
|
||||
}
|
||||
|
||||
private hitElement(
|
||||
x: number,
|
||||
y: number,
|
||||
element: ExcalidrawElement,
|
||||
considerBoundingBox = true,
|
||||
) {
|
||||
// if the element is selected, then hit test is done against its bounding box
|
||||
if (
|
||||
considerBoundingBox &&
|
||||
this.state.selectedElementIds[element.id] &&
|
||||
shouldShowBoundingBox([element], this.state)
|
||||
) {
|
||||
return hitElementBoundingBox(
|
||||
x,
|
||||
y,
|
||||
elementsMap,
|
||||
),
|
||||
).filter((element) => {
|
||||
// hitting a frame's element from outside the frame is not considered a hit
|
||||
const containingFrame = getContainingFrame(element, elementsMap);
|
||||
return containingFrame &&
|
||||
this.state.frameRendering.enabled &&
|
||||
this.state.frameRendering.clip
|
||||
? isCursorInFrame({ x, y }, containingFrame, elementsMap)
|
||||
: true;
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.getHitThreshold(),
|
||||
);
|
||||
}
|
||||
|
||||
// take bound text element into consideration for hit collision as well
|
||||
const hitBoundTextOfElement = hitElementBoundText(
|
||||
x,
|
||||
y,
|
||||
this.getBoundTextShape(element),
|
||||
);
|
||||
if (hitBoundTextOfElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hitElementItself({
|
||||
x,
|
||||
y,
|
||||
element,
|
||||
shape: this.getElementShape(element),
|
||||
threshold: this.getHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(element)
|
||||
? this.frameNameBoundsCache.get(element)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
private getTextBindableContainerAtPosition(x: number, y: number) {
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
if (selectedElements.length === 1) {
|
||||
return isTextBindableContainer(selectedElements[0], false)
|
||||
? selectedElements[0]
|
||||
: null;
|
||||
}
|
||||
let hitElement = null;
|
||||
// We need to do hit testing from front (end of the array) to back (beginning of the array)
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
if (elements[index].isDeleted) {
|
||||
continue;
|
||||
}
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||
elements[index],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (
|
||||
isArrowElement(elements[index]) &&
|
||||
hitElementItself({
|
||||
x,
|
||||
y,
|
||||
element: elements[index],
|
||||
shape: this.getElementShape(elements[index]),
|
||||
threshold: this.getHitThreshold(),
|
||||
})
|
||||
) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
} else if (x1 < x && x < x2 && y1 < y && y < y2) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||
}
|
||||
|
||||
private startTextEditing = ({
|
||||
sceneX,
|
||||
sceneY,
|
||||
|
@ -4667,25 +4844,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const container = getTextBindableContainerAtPosition(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const container = this.getTextBindableContainerAtPosition(sceneX, sceneY);
|
||||
|
||||
if (container) {
|
||||
if (
|
||||
hasBoundTextElement(container) ||
|
||||
!isTransparent(container.backgroundColor) ||
|
||||
isHittingElementNotConsideringBoundingBox(
|
||||
container,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
[sceneX, sceneY],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
hitElementItself({
|
||||
x: sceneX,
|
||||
y: sceneY,
|
||||
element: container,
|
||||
shape: this.getElementShape(container),
|
||||
threshold: this.getHitThreshold(),
|
||||
})
|
||||
) {
|
||||
const midPoint = getContainerCenter(
|
||||
container,
|
||||
|
@ -5281,7 +5452,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
const threshold = 10 / this.state.zoom.value;
|
||||
const threshold = this.getHitThreshold();
|
||||
const point = { ...pointerDownState.lastCoords };
|
||||
let samplingInterval = 0;
|
||||
while (samplingInterval <= distance) {
|
||||
|
@ -5346,7 +5517,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
|
@ -5355,13 +5525,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
let hoverPointIndex = -1;
|
||||
let segmentMidPointHoveredCoords = null;
|
||||
if (
|
||||
isHittingElementNotConsideringBoundingBox(
|
||||
hitElementItself({
|
||||
x: scenePointerX,
|
||||
y: scenePointerY,
|
||||
element,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
[scenePointerX, scenePointerY],
|
||||
elementsMap,
|
||||
)
|
||||
shape: this.getElementShape(element),
|
||||
})
|
||||
) {
|
||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
|
@ -5383,29 +5552,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
} else {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
} else if (
|
||||
shouldShowBoundingBox([element], this.state) &&
|
||||
isHittingElementBoundingBoxWithoutHittingElement(
|
||||
element,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
} else if (
|
||||
boundTextElement &&
|
||||
hitTest(
|
||||
boundTextElement,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
|
||||
|
@ -6159,8 +6306,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.history,
|
||||
pointerDownState.origin,
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
this,
|
||||
);
|
||||
if (ret.hitElement) {
|
||||
pointerDownState.hit.element = ret.hitElement;
|
||||
|
@ -6383,7 +6529,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
// How many pixels off the shape boundary we still consider a hit
|
||||
const threshold = 10 / this.state.zoom.value;
|
||||
const threshold = this.getHitThreshold();
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
return (
|
||||
point.x > x1 - threshold &&
|
||||
|
@ -6411,13 +6557,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
|
||||
// FIXME
|
||||
let container = getTextBindableContainerAtPosition(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
let container = this.getTextBindableContainerAtPosition(sceneX, sceneY);
|
||||
|
||||
if (hasBoundTextElement(element)) {
|
||||
container = element as ExcalidrawTextContainer;
|
||||
|
@ -6497,8 +6637,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
pointerDownState.origin,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this,
|
||||
);
|
||||
this.scene.addNewElement(element);
|
||||
this.setState({
|
||||
|
@ -6766,8 +6905,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
pointerDownState.origin,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this,
|
||||
);
|
||||
|
||||
this.scene.addNewElement(element);
|
||||
|
@ -7551,7 +7689,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
? this.state.editingElement
|
||||
: null,
|
||||
snapLines: updateStable(prevState.snapLines, []),
|
||||
|
||||
originSnapOffset: null,
|
||||
}));
|
||||
|
||||
|
@ -7578,8 +7715,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
childEvent,
|
||||
this.state.editingLinearElement,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
this,
|
||||
);
|
||||
if (editingLinearElement !== this.state.editingLinearElement) {
|
||||
this.setState({
|
||||
|
@ -7603,8 +7739,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
childEvent,
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
this,
|
||||
);
|
||||
|
||||
const { startBindingElement, endBindingElement } =
|
||||
|
@ -7753,9 +7888,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
maybeBindLinearElement(
|
||||
draggingElement,
|
||||
this.state,
|
||||
this.scene,
|
||||
pointerCoords,
|
||||
elementsMap,
|
||||
this,
|
||||
);
|
||||
}
|
||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||
|
@ -8207,16 +8341,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
// not dragged
|
||||
!pointerDownState.drag.hasOccurred &&
|
||||
// not resized
|
||||
!this.state.isResizing &&
|
||||
// only hitting the bounding box of the previous hit element
|
||||
((hitElement &&
|
||||
isHittingElementBoundingBoxWithoutHittingElement(
|
||||
hitElement,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
hitElementBoundingBoxOnly(
|
||||
{
|
||||
x: pointerDownState.origin.x,
|
||||
y: pointerDownState.origin.y,
|
||||
element: hitElement,
|
||||
shape: this.getElementShape(hitElement),
|
||||
threshold: this.getHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(hitElement)
|
||||
? this.frameNameBoundsCache.get(hitElement)
|
||||
: null,
|
||||
},
|
||||
elementsMap,
|
||||
)) ||
|
||||
(!hitElement &&
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
|
||||
|
@ -8232,6 +8374,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
activeEmbeddable: null,
|
||||
});
|
||||
}
|
||||
// reset cursor
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -8267,11 +8411,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
isBindingEnabled(this.state)
|
||||
? bindOrUnbindSelectedElements(
|
||||
this.scene.getSelectedElements(this.state),
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
this,
|
||||
)
|
||||
: unbindLinearElements(
|
||||
this.scene.getSelectedElements(this.state),
|
||||
this.scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
@ -8758,8 +8901,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}): void => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this,
|
||||
);
|
||||
this.setState({
|
||||
suggestedBindings:
|
||||
|
@ -8786,8 +8928,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
||||
const hoveredBindableElement = getHoveredElementForBinding(
|
||||
coords,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this,
|
||||
);
|
||||
if (
|
||||
hoveredBindableElement != null &&
|
||||
|
@ -8815,8 +8956,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
const suggestedBindings = getEligibleElementsForBinding(
|
||||
selectedElements,
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this,
|
||||
);
|
||||
this.setState({ suggestedBindings });
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ import clsx from "clsx";
|
|||
import { KEYS } from "../../keys";
|
||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY } from "../../constants";
|
||||
import { getElementAbsoluteCoords } from "../../element/bounds";
|
||||
import { getTooltipDiv, updateTooltipPosition } from "../Tooltip";
|
||||
import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
|
||||
import { getSelectedElements } from "../../scene";
|
||||
import { isPointHittingElementBoundingBox } from "../../element/collision";
|
||||
import { hitElementBoundingBox } from "../../element/collision";
|
||||
import { isLocalLink, normalizeLink } from "../../data/url";
|
||||
|
||||
import "./Hyperlink.scss";
|
||||
|
@ -425,15 +425,7 @@ const shouldHideLinkPopup = (
|
|||
|
||||
const threshold = 15 / appState.zoom.value;
|
||||
// hitbox to prevent hiding when hovered in element bounding box
|
||||
if (
|
||||
isPointHittingElementBoundingBox(
|
||||
element,
|
||||
elementsMap,
|
||||
[sceneX, sceneY],
|
||||
threshold,
|
||||
null,
|
||||
)
|
||||
) {
|
||||
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
|
||||
return false;
|
||||
}
|
||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MIME_TYPES } from "../../constants";
|
||||
import { Bounds, getElementAbsoluteCoords } from "../../element/bounds";
|
||||
import { isPointHittingElementBoundingBox } from "../../element/collision";
|
||||
import { hitElementBoundingBox } from "../../element/collision";
|
||||
import { ElementsMap, NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import { rotate } from "../../math";
|
||||
import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
|
||||
|
@ -75,17 +75,10 @@ export const isPointHittingLink = (
|
|||
if (!element.link || appState.selectedElementIds[element.id]) {
|
||||
return false;
|
||||
}
|
||||
const threshold = 4 / appState.zoom.value;
|
||||
if (
|
||||
!isMobile &&
|
||||
appState.viewModeEnabled &&
|
||||
isPointHittingElementBoundingBox(
|
||||
element,
|
||||
elementsMap,
|
||||
[x, y],
|
||||
threshold,
|
||||
null,
|
||||
)
|
||||
hitElementBoundingBox(x, y, element, elementsMap)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,37 @@
|
|||
import * as GA from "../ga";
|
||||
import * as GAPoint from "../gapoints";
|
||||
import * as GADirection from "../gadirections";
|
||||
import * as GALine from "../galines";
|
||||
import * as GATransform from "../gatransforms";
|
||||
|
||||
import {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawBindableElement,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
PointBinding,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
NonDeleted,
|
||||
ExcalidrawLinearElement,
|
||||
PointBinding,
|
||||
NonDeletedExcalidrawElement,
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
import { AppClassProperties, AppState, Point } from "../types";
|
||||
import { isPointOnShape } from "../../utils/collision";
|
||||
import { getElementAtPosition } from "../scene";
|
||||
import { AppState } from "../types";
|
||||
import {
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
isLinearElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
bindingBorderTest,
|
||||
distanceToBindableElement,
|
||||
maxBindingGap,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
determineFocusPoint,
|
||||
} from "./collision";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import Scene from "../scene/Scene";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
@ -152,29 +161,22 @@ const bindOrUnbindLinearElementEdge = (
|
|||
|
||||
export const bindOrUnbindSelectedElements = (
|
||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): void => {
|
||||
selectedElements.forEach((selectedElement) => {
|
||||
if (isBindingElement(selectedElement)) {
|
||||
bindOrUnbindLinearElement(
|
||||
selectedElement,
|
||||
getElligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elements,
|
||||
elementsMap,
|
||||
),
|
||||
getElligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elements,
|
||||
elementsMap,
|
||||
),
|
||||
elementsMap,
|
||||
getElligibleElementForBindingElement(selectedElement, "start", app),
|
||||
getElligibleElementForBindingElement(selectedElement, "end", app),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
} else if (isBindableElement(selectedElement)) {
|
||||
maybeBindBindableElement(selectedElement, elementsMap);
|
||||
maybeBindBindableElement(
|
||||
selectedElement,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -182,40 +184,34 @@ export const bindOrUnbindSelectedElements = (
|
|||
const maybeBindBindableElement = (
|
||||
bindableElement: NonDeleted<ExcalidrawBindableElement>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): void => {
|
||||
getElligibleElementsForBindableElementAndWhere(
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
).forEach(([linearElement, where]) =>
|
||||
bindOrUnbindLinearElement(
|
||||
linearElement,
|
||||
where === "end" ? "keep" : bindableElement,
|
||||
where === "start" ? "keep" : bindableElement,
|
||||
elementsMap,
|
||||
),
|
||||
getElligibleElementsForBindableElementAndWhere(bindableElement, app).forEach(
|
||||
([linearElement, where]) =>
|
||||
bindOrUnbindLinearElement(
|
||||
linearElement,
|
||||
where === "end" ? "keep" : bindableElement,
|
||||
where === "start" ? "keep" : bindableElement,
|
||||
elementsMap,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const maybeBindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
scene: Scene,
|
||||
pointerCoords: { x: number; y: number },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): void => {
|
||||
if (appState.startBoundElement != null) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
appState.startBoundElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
}
|
||||
const hoveredElement = getHoveredElementForBinding(
|
||||
pointerCoords,
|
||||
scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
);
|
||||
const hoveredElement = getHoveredElementForBinding(pointerCoords, app);
|
||||
if (
|
||||
hoveredElement != null &&
|
||||
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
|
||||
|
@ -224,7 +220,12 @@ export const maybeBindLinearElement = (
|
|||
"end",
|
||||
)
|
||||
) {
|
||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
"end",
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -283,7 +284,7 @@ export const isLinearElementSimpleAndAlreadyBound = (
|
|||
};
|
||||
|
||||
export const unbindLinearElements = (
|
||||
elements: NonDeleted<ExcalidrawElement>[],
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
): void => {
|
||||
elements.forEach((element) => {
|
||||
|
@ -311,14 +312,13 @@ export const getHoveredElementForBinding = (
|
|||
x: number;
|
||||
y: number;
|
||||
},
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||
const hoveredElement = getElementAtPosition(
|
||||
elements,
|
||||
app.scene.getNonDeletedElements(),
|
||||
(element) =>
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(element, pointerCoords, elementsMap),
|
||||
bindingBorderTest(element, pointerCoords, app),
|
||||
);
|
||||
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
|
||||
};
|
||||
|
@ -547,23 +547,21 @@ const maybeCalculateNewGapWhenScaling = (
|
|||
// TODO: this is a bottleneck, optimise
|
||||
export const getEligibleElementsForBinding = (
|
||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): SuggestedBinding[] => {
|
||||
const includedElementIds = new Set(selectedElements.map(({ id }) => id));
|
||||
return selectedElements.flatMap((selectedElement) =>
|
||||
isBindingElement(selectedElement, false)
|
||||
? (getElligibleElementsForBindingElement(
|
||||
selectedElement as NonDeleted<ExcalidrawLinearElement>,
|
||||
elements,
|
||||
elementsMap,
|
||||
app,
|
||||
).filter(
|
||||
(element) => !includedElementIds.has(element.id),
|
||||
) as SuggestedBinding[])
|
||||
: isBindableElement(selectedElement, false)
|
||||
? getElligibleElementsForBindableElementAndWhere(
|
||||
selectedElement,
|
||||
elementsMap,
|
||||
app,
|
||||
).filter((binding) => !includedElementIds.has(binding[0].id))
|
||||
: [],
|
||||
);
|
||||
|
@ -571,22 +569,11 @@ export const getEligibleElementsForBinding = (
|
|||
|
||||
const getElligibleElementsForBindingElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||
return [
|
||||
getElligibleElementForBindingElement(
|
||||
linearElement,
|
||||
"start",
|
||||
elements,
|
||||
elementsMap,
|
||||
),
|
||||
getElligibleElementForBindingElement(
|
||||
linearElement,
|
||||
"end",
|
||||
elements,
|
||||
elementsMap,
|
||||
),
|
||||
getElligibleElementForBindingElement(linearElement, "start", app),
|
||||
getElligibleElementForBindingElement(linearElement, "end", app),
|
||||
].filter(
|
||||
(element): element is NonDeleted<ExcalidrawBindableElement> =>
|
||||
element != null,
|
||||
|
@ -596,13 +583,15 @@ const getElligibleElementsForBindingElement = (
|
|||
const getElligibleElementForBindingElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||
return getHoveredElementForBinding(
|
||||
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
||||
elements,
|
||||
elementsMap,
|
||||
getLinearElementEdgeCoors(
|
||||
linearElement,
|
||||
startOrEnd,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
app,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -623,7 +612,7 @@ const getLinearElementEdgeCoors = (
|
|||
|
||||
const getElligibleElementsForBindableElementAndWhere = (
|
||||
bindableElement: NonDeleted<ExcalidrawBindableElement>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): SuggestedPointBinding[] => {
|
||||
const scene = Scene.getScene(bindableElement)!;
|
||||
return scene
|
||||
|
@ -636,13 +625,15 @@ const getElligibleElementsForBindableElementAndWhere = (
|
|||
element,
|
||||
"start",
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
app,
|
||||
);
|
||||
const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
|
||||
element,
|
||||
"end",
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
app,
|
||||
);
|
||||
if (!canBindStart && !canBindEnd) {
|
||||
return null;
|
||||
|
@ -661,6 +652,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
|
|||
startOrEnd: "start" | "end",
|
||||
bindableElement: NonDeleted<ExcalidrawBindableElement>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): boolean => {
|
||||
const existingBinding =
|
||||
linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
|
||||
|
@ -674,7 +666,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
|
|||
bindingBorderTest(
|
||||
bindableElement,
|
||||
getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
|
||||
elementsMap,
|
||||
app,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -846,3 +838,547 @@ const newBoundElementsAfterDeletion = (
|
|||
}
|
||||
return boundElements.filter((ele) => !deletedElementIds.has(ele.id));
|
||||
};
|
||||
|
||||
export const bindingBorderTest = (
|
||||
element: NonDeleted<ExcalidrawBindableElement>,
|
||||
{ x, y }: { x: number; y: number },
|
||||
app: AppClassProperties,
|
||||
): boolean => {
|
||||
const threshold = maxBindingGap(element, element.width, element.height);
|
||||
const shape = app.getElementShape(element);
|
||||
return isPointOnShape([x, y], shape, threshold);
|
||||
};
|
||||
|
||||
export const maxBindingGap = (
|
||||
element: ExcalidrawElement,
|
||||
elementWidth: number,
|
||||
elementHeight: number,
|
||||
): number => {
|
||||
// Aligns diamonds with rectangles
|
||||
const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1;
|
||||
const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight);
|
||||
// We make the bindable boundary bigger for bigger elements
|
||||
return Math.max(16, Math.min(0.25 * smallerDimension, 32));
|
||||
};
|
||||
|
||||
export const distanceToBindableElement = (
|
||||
element: ExcalidrawBindableElement,
|
||||
point: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): number => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
return distanceToRectangle(element, point, elementsMap);
|
||||
case "diamond":
|
||||
return distanceToDiamond(element, point, elementsMap);
|
||||
case "ellipse":
|
||||
return distanceToEllipse(element, point, elementsMap);
|
||||
}
|
||||
};
|
||||
|
||||
const distanceToRectangle = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
point: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||
element,
|
||||
point,
|
||||
elementsMap,
|
||||
);
|
||||
return Math.max(
|
||||
GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
|
||||
GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
|
||||
);
|
||||
};
|
||||
|
||||
const distanceToDiamond = (
|
||||
element: ExcalidrawDiamondElement,
|
||||
point: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||
element,
|
||||
point,
|
||||
elementsMap,
|
||||
);
|
||||
const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
|
||||
return GAPoint.distanceToLine(pointRel, side);
|
||||
};
|
||||
|
||||
export const distanceToEllipse = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
point: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): number => {
|
||||
const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
|
||||
return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
|
||||
};
|
||||
|
||||
const ellipseParamsForTest = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
point: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): [GA.Point, GA.Line] => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(
|
||||
element,
|
||||
point,
|
||||
elementsMap,
|
||||
);
|
||||
const [px, py] = GAPoint.toTuple(pointRel);
|
||||
|
||||
// We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
|
||||
let tx = 0.707;
|
||||
let ty = 0.707;
|
||||
|
||||
const a = hwidth;
|
||||
const b = hheight;
|
||||
|
||||
// This is a numerical method to find the params tx, ty at which
|
||||
// the ellipse has the closest point to the given point
|
||||
[0, 1, 2, 3].forEach((_) => {
|
||||
const xx = a * tx;
|
||||
const yy = b * ty;
|
||||
|
||||
const ex = ((a * a - b * b) * tx ** 3) / a;
|
||||
const ey = ((b * b - a * a) * ty ** 3) / b;
|
||||
|
||||
const rx = xx - ex;
|
||||
const ry = yy - ey;
|
||||
|
||||
const qx = px - ex;
|
||||
const qy = py - ey;
|
||||
|
||||
const r = Math.hypot(ry, rx);
|
||||
const q = Math.hypot(qy, qx);
|
||||
|
||||
tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
|
||||
ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
|
||||
const t = Math.hypot(ty, tx);
|
||||
tx /= t;
|
||||
ty /= t;
|
||||
});
|
||||
|
||||
const closestPoint = GA.point(a * tx, b * ty);
|
||||
|
||||
const tangent = GALine.orthogonalThrough(pointRel, closestPoint);
|
||||
return [pointRel, tangent];
|
||||
};
|
||||
|
||||
// Returns:
|
||||
// 1. the point relative to the elements (x, y) position
|
||||
// 2. the point relative to the element's center with positive (x, y)
|
||||
// 3. half element width
|
||||
// 4. half element height
|
||||
//
|
||||
// Note that for linear elements the (x, y) position is not at the
|
||||
// top right corner of their boundary.
|
||||
//
|
||||
// Rectangles, diamonds and ellipses are symmetrical over axes,
|
||||
// and other elements have a rectangular boundary,
|
||||
// so we only need to perform hit tests for the positive quadrant.
|
||||
const pointRelativeToElement = (
|
||||
element: ExcalidrawElement,
|
||||
pointTuple: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): [GA.Point, GA.Point, number, number] => {
|
||||
const point = GAPoint.from(pointTuple);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const center = coordsCenter(x1, y1, x2, y2);
|
||||
// GA has angle orientation opposite to `rotate`
|
||||
const rotate = GATransform.rotation(center, element.angle);
|
||||
const pointRotated = GATransform.apply(rotate, point);
|
||||
const pointRelToCenter = GA.sub(pointRotated, GADirection.from(center));
|
||||
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
|
||||
const elementPos = GA.offset(element.x, element.y);
|
||||
const pointRelToPos = GA.sub(pointRotated, elementPos);
|
||||
const halfWidth = (x2 - x1) / 2;
|
||||
const halfHeight = (y2 - y1) / 2;
|
||||
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
|
||||
};
|
||||
|
||||
const relativizationToElementCenter = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): GA.Transform => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const center = coordsCenter(x1, y1, x2, y2);
|
||||
// GA has angle orientation opposite to `rotate`
|
||||
const rotate = GATransform.rotation(center, element.angle);
|
||||
const translate = GA.reverse(
|
||||
GATransform.translation(GADirection.from(center)),
|
||||
);
|
||||
return GATransform.compose(rotate, translate);
|
||||
};
|
||||
|
||||
const coordsCenter = (
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
): GA.Point => {
|
||||
return GA.point((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
};
|
||||
|
||||
// The focus distance is the oriented ratio between the size of
|
||||
// the `element` and the "focus image" of the element on which
|
||||
// all focus points lie, so it's a number between -1 and 1.
|
||||
// The line going through `a` and `b` is a tangent to the "focus image"
|
||||
// of the element.
|
||||
export const determineFocusDistance = (
|
||||
element: ExcalidrawBindableElement,
|
||||
// Point on the line, in absolute coordinates
|
||||
a: Point,
|
||||
// Another point on the line, in absolute coordinates (closer to element)
|
||||
b: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): number => {
|
||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
|
||||
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
|
||||
const line = GALine.through(aRel, bRel);
|
||||
const q = element.height / element.width;
|
||||
const hwidth = element.width / 2;
|
||||
const hheight = element.height / 2;
|
||||
const n = line[2];
|
||||
const m = line[3];
|
||||
const c = line[1];
|
||||
const mabs = Math.abs(m);
|
||||
const nabs = Math.abs(n);
|
||||
let ret;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
ret = c / (hwidth * (nabs + q * mabs));
|
||||
break;
|
||||
case "diamond":
|
||||
ret = mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||
break;
|
||||
case "ellipse":
|
||||
ret = c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
|
||||
break;
|
||||
}
|
||||
return ret || 0;
|
||||
};
|
||||
|
||||
export const determineFocusPoint = (
|
||||
element: ExcalidrawBindableElement,
|
||||
// The oriented, relative distance from the center of `element` of the
|
||||
// returned focusPoint
|
||||
focus: number,
|
||||
adjecentPoint: Point,
|
||||
elementsMap: ElementsMap,
|
||||
): Point => {
|
||||
if (focus === 0) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const center = coordsCenter(x1, y1, x2, y2);
|
||||
return GAPoint.toTuple(center);
|
||||
}
|
||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||
const adjecentPointRel = GATransform.apply(
|
||||
relateToCenter,
|
||||
GAPoint.from(adjecentPoint),
|
||||
);
|
||||
const reverseRelateToCenter = GA.reverse(relateToCenter);
|
||||
let point;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
break;
|
||||
case "ellipse":
|
||||
point = findFocusPointForEllipse(element, focus, adjecentPointRel);
|
||||
break;
|
||||
}
|
||||
return GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point));
|
||||
};
|
||||
|
||||
// Returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
// and the `element`, in ascending order of distance from `a`.
|
||||
export const intersectElementWithLine = (
|
||||
element: ExcalidrawBindableElement,
|
||||
// Point on the line, in absolute coordinates
|
||||
a: Point,
|
||||
// Another point on the line, in absolute coordinates
|
||||
b: Point,
|
||||
// If given, the element is inflated by this value
|
||||
gap: number = 0,
|
||||
elementsMap: ElementsMap,
|
||||
): Point[] => {
|
||||
const relateToCenter = relativizationToElementCenter(element, elementsMap);
|
||||
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
|
||||
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
|
||||
const line = GALine.through(aRel, bRel);
|
||||
const reverseRelateToCenter = GA.reverse(relateToCenter);
|
||||
const intersections = getSortedElementLineIntersections(
|
||||
element,
|
||||
line,
|
||||
aRel,
|
||||
gap,
|
||||
);
|
||||
return intersections.map((point) =>
|
||||
GAPoint.toTuple(GATransform.apply(reverseRelateToCenter, point)),
|
||||
);
|
||||
};
|
||||
|
||||
const getSortedElementLineIntersections = (
|
||||
element: ExcalidrawBindableElement,
|
||||
// Relative to element center
|
||||
line: GA.Line,
|
||||
// Relative to element center
|
||||
nearPoint: GA.Point,
|
||||
gap: number = 0,
|
||||
): GA.Point[] => {
|
||||
let intersections: GA.Point[];
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
const corners = getCorners(element);
|
||||
intersections = corners
|
||||
.flatMap((point, i) => {
|
||||
const edge: [GA.Point, GA.Point] = [point, corners[(i + 1) % 4]];
|
||||
return intersectSegment(line, offsetSegment(edge, gap));
|
||||
})
|
||||
.concat(
|
||||
corners.flatMap((point) => getCircleIntersections(point, gap, line)),
|
||||
);
|
||||
break;
|
||||
case "ellipse":
|
||||
intersections = getEllipseIntersections(element, gap, line);
|
||||
break;
|
||||
}
|
||||
if (intersections.length < 2) {
|
||||
// Ignore the "edge" case of only intersecting with a single corner
|
||||
return [];
|
||||
}
|
||||
const sortedIntersections = intersections.sort(
|
||||
(i1, i2) =>
|
||||
GAPoint.distance(i1, nearPoint) - GAPoint.distance(i2, nearPoint),
|
||||
);
|
||||
return [
|
||||
sortedIntersections[0],
|
||||
sortedIntersections[sortedIntersections.length - 1],
|
||||
];
|
||||
};
|
||||
|
||||
const getCorners = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
scale: number = 1,
|
||||
): GA.Point[] => {
|
||||
const hx = (scale * element.width) / 2;
|
||||
const hy = (scale * element.height) / 2;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
GA.point(hx, -hy),
|
||||
GA.point(-hx, -hy),
|
||||
GA.point(-hx, hy),
|
||||
];
|
||||
case "diamond":
|
||||
return [
|
||||
GA.point(0, hy),
|
||||
GA.point(hx, 0),
|
||||
GA.point(0, -hy),
|
||||
GA.point(-hx, 0),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// Returns intersection of `line` with `segment`, with `segment` moved by
|
||||
// `gap` in its polar direction.
|
||||
// If intersection coincides with second segment point returns empty array.
|
||||
const intersectSegment = (
|
||||
line: GA.Line,
|
||||
segment: [GA.Point, GA.Point],
|
||||
): GA.Point[] => {
|
||||
const [a, b] = segment;
|
||||
const aDist = GAPoint.distanceToLine(a, line);
|
||||
const bDist = GAPoint.distanceToLine(b, line);
|
||||
if (aDist * bDist >= 0) {
|
||||
// The intersection is outside segment `(a, b)`
|
||||
return [];
|
||||
}
|
||||
return [GAPoint.intersect(line, GALine.through(a, b))];
|
||||
};
|
||||
|
||||
const offsetSegment = (
|
||||
segment: [GA.Point, GA.Point],
|
||||
distance: number,
|
||||
): [GA.Point, GA.Point] => {
|
||||
const [a, b] = segment;
|
||||
const offset = GATransform.translationOrthogonal(
|
||||
GADirection.fromTo(a, b),
|
||||
distance,
|
||||
);
|
||||
return [GATransform.apply(offset, a), GATransform.apply(offset, b)];
|
||||
};
|
||||
|
||||
const getEllipseIntersections = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
gap: number,
|
||||
line: GA.Line,
|
||||
): GA.Point[] => {
|
||||
const a = element.width / 2 + gap;
|
||||
const b = element.height / 2 + gap;
|
||||
const m = line[2];
|
||||
const n = line[3];
|
||||
const c = line[1];
|
||||
const squares = a * a * m * m + b * b * n * n;
|
||||
const discr = squares - c * c;
|
||||
if (squares === 0 || discr <= 0) {
|
||||
return [];
|
||||
}
|
||||
const discrRoot = Math.sqrt(discr);
|
||||
const xn = -a * a * m * c;
|
||||
const yn = -b * b * n * c;
|
||||
return [
|
||||
GA.point(
|
||||
(xn + a * b * n * discrRoot) / squares,
|
||||
(yn - a * b * m * discrRoot) / squares,
|
||||
),
|
||||
GA.point(
|
||||
(xn - a * b * n * discrRoot) / squares,
|
||||
(yn + a * b * m * discrRoot) / squares,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const getCircleIntersections = (
|
||||
center: GA.Point,
|
||||
radius: number,
|
||||
line: GA.Line,
|
||||
): GA.Point[] => {
|
||||
if (radius === 0) {
|
||||
return GAPoint.distanceToLine(line, center) === 0 ? [center] : [];
|
||||
}
|
||||
const m = line[2];
|
||||
const n = line[3];
|
||||
const c = line[1];
|
||||
const [a, b] = GAPoint.toTuple(center);
|
||||
const r = radius;
|
||||
const squares = m * m + n * n;
|
||||
const discr = r * r * squares - (m * a + n * b + c) ** 2;
|
||||
if (squares === 0 || discr <= 0) {
|
||||
return [];
|
||||
}
|
||||
const discrRoot = Math.sqrt(discr);
|
||||
const xn = a * n * n - b * m * n - m * c;
|
||||
const yn = b * m * m - a * m * n - n * c;
|
||||
|
||||
return [
|
||||
GA.point((xn + n * discrRoot) / squares, (yn - m * discrRoot) / squares),
|
||||
GA.point((xn - n * discrRoot) / squares, (yn + m * discrRoot) / squares),
|
||||
];
|
||||
};
|
||||
|
||||
// The focus point is the tangent point of the "focus image" of the
|
||||
// `element`, where the tangent goes through `point`.
|
||||
export const findFocusPointForEllipse = (
|
||||
ellipse: ExcalidrawEllipseElement,
|
||||
// Between -1 and 1 (not 0) the relative size of the "focus image" of
|
||||
// the element on which the focus point lies
|
||||
relativeDistance: number,
|
||||
// The point for which we're trying to find the focus point, relative
|
||||
// to the ellipse center.
|
||||
point: GA.Point,
|
||||
): GA.Point => {
|
||||
const relativeDistanceAbs = Math.abs(relativeDistance);
|
||||
const a = (ellipse.width * relativeDistanceAbs) / 2;
|
||||
const b = (ellipse.height * relativeDistanceAbs) / 2;
|
||||
|
||||
const orientation = Math.sign(relativeDistance);
|
||||
const [px, pyo] = GAPoint.toTuple(point);
|
||||
|
||||
// The calculation below can't handle py = 0
|
||||
const py = pyo === 0 ? 0.0001 : pyo;
|
||||
|
||||
const squares = px ** 2 * b ** 2 + py ** 2 * a ** 2;
|
||||
// Tangent mx + ny + 1 = 0
|
||||
const m =
|
||||
(-px * b ** 2 +
|
||||
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
|
||||
squares;
|
||||
|
||||
let n = (-m * px - 1) / py;
|
||||
|
||||
if (n === 0) {
|
||||
// if zero {-0, 0}, fall back to a same-sign value in the similar range
|
||||
n = (Object.is(n, -0) ? -1 : 1) * 0.01;
|
||||
}
|
||||
|
||||
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
|
||||
return GA.point(x, (-m * x - 1) / n);
|
||||
};
|
||||
|
||||
export const findFocusPointForRectangulars = (
|
||||
element:
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
// to the size of the element. Sign determines orientation.
|
||||
relativeDistance: number,
|
||||
// The point for which we're trying to find the focus point, relative
|
||||
// to the element center.
|
||||
point: GA.Point,
|
||||
): GA.Point => {
|
||||
const relativeDistanceAbs = Math.abs(relativeDistance);
|
||||
const orientation = Math.sign(relativeDistance);
|
||||
const corners = getCorners(element, relativeDistanceAbs);
|
||||
|
||||
let maxDistance = 0;
|
||||
let tangentPoint: null | GA.Point = null;
|
||||
corners.forEach((corner) => {
|
||||
const distance = orientation * GALine.through(point, corner)[1];
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance;
|
||||
tangentPoint = corner;
|
||||
}
|
||||
});
|
||||
return tangentPoint!;
|
||||
};
|
||||
|
|
|
@ -299,13 +299,6 @@ export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
|
|||
];
|
||||
};
|
||||
|
||||
export const pointRelativeTo = (
|
||||
element: ExcalidrawElement,
|
||||
absoluteCoords: Point,
|
||||
): Point => {
|
||||
return [absoluteCoords[0] - element.x, absoluteCoords[1] - element.y];
|
||||
};
|
||||
|
||||
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
||||
// Here we add +1 to avoid these numbers to be 0
|
||||
// otherwise rough.js will throw an error complaining about it
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -29,10 +29,6 @@ export {
|
|||
getTransformHandlesFromCoords,
|
||||
getTransformHandles,
|
||||
} from "./transformHandles";
|
||||
export {
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
} from "./collision";
|
||||
export {
|
||||
resizeTest,
|
||||
getCursorForResizingElement,
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
ExcalidrawBindableElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
import {
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
AppState,
|
||||
PointerCoords,
|
||||
InteractiveCanvasAppState,
|
||||
AppClassProperties,
|
||||
} from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import History from "../history";
|
||||
|
@ -334,9 +334,10 @@ export class LinearElementEditor {
|
|||
event: PointerEvent,
|
||||
editingLinearElement: LinearElementEditor,
|
||||
appState: AppState,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): LinearElementEditor {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
|
||||
const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
|
||||
editingLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
@ -380,8 +381,7 @@ export class LinearElementEditor {
|
|||
elementsMap,
|
||||
),
|
||||
),
|
||||
elements,
|
||||
elementsMap,
|
||||
app,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
@ -645,13 +645,14 @@ export class LinearElementEditor {
|
|||
history: History,
|
||||
scenePointer: { x: number; y: number },
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
app: AppClassProperties,
|
||||
): {
|
||||
didAddPoint: boolean;
|
||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||
linearElementEditor: LinearElementEditor | null;
|
||||
} {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
|
||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||
didAddPoint: false,
|
||||
hitElement: null,
|
||||
|
@ -714,11 +715,7 @@ export class LinearElementEditor {
|
|||
},
|
||||
selectedPointsIndices: [element.points.length - 1],
|
||||
lastUncommittedPoint: null,
|
||||
endBindingElement: getHoveredElementForBinding(
|
||||
scenePointer,
|
||||
elements,
|
||||
elementsMap,
|
||||
),
|
||||
endBindingElement: getHoveredElementForBinding(scenePointer, app),
|
||||
};
|
||||
|
||||
ret.didAddPoint = true;
|
||||
|
|
|
@ -26,16 +26,11 @@ import { isTextElement } from ".";
|
|||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { AppState } from "../types";
|
||||
import { isTextBindableContainer } from "./typeChecks";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||
|
||||
import { ExtractSetType, MakeBrand } from "../utility-types";
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import { ExtractSetType, MakeBrand } from "../utility-types";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
|
@ -771,50 +766,6 @@ export const suppportsHorizontalAlign = (
|
|||
});
|
||||
};
|
||||
|
||||
export const getTextBindableContainerAtPosition = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawTextContainer | null => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
if (selectedElements.length === 1) {
|
||||
return isTextBindableContainer(selectedElements[0], false)
|
||||
? selectedElements[0]
|
||||
: null;
|
||||
}
|
||||
let hitElement = null;
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
if (elements[index].isDeleted) {
|
||||
continue;
|
||||
}
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||
elements[index],
|
||||
elementsMap,
|
||||
);
|
||||
if (
|
||||
isArrowElement(elements[index]) &&
|
||||
isHittingElementNotConsideringBoundingBox(
|
||||
elements[index],
|
||||
appState,
|
||||
null,
|
||||
[x, y],
|
||||
elementsMap,
|
||||
)
|
||||
) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
} else if (x1 < x && x < x2 && y1 < y && y < y2) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||
};
|
||||
|
||||
const VALID_CONTAINER_TYPES = new Set([
|
||||
"rectangle",
|
||||
"ellipse",
|
||||
|
|
|
@ -34,8 +34,11 @@ import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants";
|
|||
|
||||
import { renderSnaps } from "../renderer/renderSnaps";
|
||||
|
||||
import { maxBindingGap } from "../element/collision";
|
||||
import { SuggestedBinding, SuggestedPointBinding } from "../element/binding";
|
||||
import {
|
||||
maxBindingGap,
|
||||
SuggestedBinding,
|
||||
SuggestedPointBinding,
|
||||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
|
|
|
@ -2294,14 +2294,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2354,14 +2354,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2396,14 +2396,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2540,14 +2540,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2573,14 +2573,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
|
@ -2633,14 +2633,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2677,14 +2677,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2707,14 +2707,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
|
@ -2858,14 +2858,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2893,14 +2893,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -2953,14 +2953,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -2997,14 +2997,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3027,14 +3027,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 238820263,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -3076,14 +3076,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3108,14 +3108,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -3254,14 +3254,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 640725609,
|
||||
"versionNonce": 1315507081,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3287,14 +3287,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 760410951,
|
||||
"seed": 747212839,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1315507081,
|
||||
"versionNonce": 1006504105,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -3347,14 +3347,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3391,14 +3391,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3421,13 +3421,87 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"appState": {
|
||||
"editingGroupId": null,
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 238820263,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
|
@ -3465,21 +3539,21 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
|
@ -3495,13 +3569,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
|
@ -3539,14 +3613,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3556,7 +3630,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"backgroundColor": "#a5d8ff",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fillStyle": "cross-hatch",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
|
@ -3569,13 +3643,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"version": 5,
|
||||
"versionNonce": 23633383,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
|
@ -3613,14 +3687,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3643,13 +3717,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "solid",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"version": 6,
|
||||
"versionNonce": 915032327,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
|
@ -3687,88 +3761,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "cross-hatch",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"versionNonce": 747212839,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"appState": {
|
||||
"editingGroupId": null,
|
||||
"editingLinearElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
"selectedElementIds": {
|
||||
"id1": true,
|
||||
},
|
||||
"selectedGroupIds": {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3791,14 +3791,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 760410951,
|
||||
"seed": 747212839,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 1006504105,
|
||||
"versionNonce": 1723083209,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -3835,14 +3835,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3865,14 +3865,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 760410951,
|
||||
"seed": 747212839,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1315507081,
|
||||
"versionNonce": 1006504105,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -3909,14 +3909,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 640725609,
|
||||
"versionNonce": 1315507081,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -3939,14 +3939,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 760410951,
|
||||
"seed": 747212839,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1315507081,
|
||||
"versionNonce": 1006504105,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -4468,14 +4468,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 238820263,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -4501,14 +4501,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -4561,14 +4561,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -4605,14 +4605,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -4635,14 +4635,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 238820263,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -4679,14 +4679,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1116226695,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 238820263,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
|
@ -4709,14 +4709,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 453191,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -6115,14 +6115,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -6148,14 +6148,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 238820263,
|
||||
"seed": 1116226695,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 238820263,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
|
@ -6208,14 +6208,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -6252,14 +6252,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 453191,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1116226695,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
|
@ -6282,14 +6282,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 238820263,
|
||||
"seed": 1116226695,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 238820263,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 0,
|
||||
|
|
|
@ -287,9 +287,16 @@ const transform = (
|
|||
keyboardModifiers: KeyboardModifiers = {},
|
||||
) => {
|
||||
const elements = Array.isArray(element) ? element : [element];
|
||||
mouse.select(elements);
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce(
|
||||
(acc, e) => ({
|
||||
...acc,
|
||||
[e.id]: true,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
let handleCoords: TransformHandle | undefined;
|
||||
|
||||
if (elements.length === 1) {
|
||||
handleCoords = getTransformHandles(
|
||||
elements[0],
|
||||
|
|
|
@ -321,9 +321,9 @@ describe("Test Linear Elements", () => {
|
|||
fireEvent.click(screen.getByTitle("Round"));
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`10`,
|
||||
`9`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||
h.elements[0] as ExcalidrawLinearElement,
|
||||
|
@ -379,9 +379,9 @@ describe("Test Linear Elements", () => {
|
|||
drag(startPoint, endPoint);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`13`,
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
|
||||
expect([line.x, line.y]).toEqual([
|
||||
points[0][0] + deltaX,
|
||||
|
@ -441,9 +441,9 @@ describe("Test Linear Elements", () => {
|
|||
]);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`17`,
|
||||
`16`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
|
@ -492,9 +492,9 @@ describe("Test Linear Elements", () => {
|
|||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`13`,
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
|
@ -533,9 +533,9 @@ describe("Test Linear Elements", () => {
|
|||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`13`,
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
|
@ -581,9 +581,9 @@ describe("Test Linear Elements", () => {
|
|||
deletePoint(points[2]);
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`19`,
|
||||
`18`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
|
@ -631,9 +631,9 @@ describe("Test Linear Elements", () => {
|
|||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`17`,
|
||||
`16`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
|
@ -729,9 +729,9 @@ describe("Test Linear Elements", () => {
|
|||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`13`,
|
||||
`12`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
|
|
|
@ -586,6 +586,7 @@ export type AppClassProperties = {
|
|||
setOpenDialog: App["setOpenDialog"];
|
||||
insertEmbeddableElement: App["insertEmbeddableElement"];
|
||||
onMagicframeToolSelect: App["onMagicframeToolSelect"];
|
||||
getElementShape: App["getElementShape"];
|
||||
getName: App["getName"];
|
||||
};
|
||||
|
||||
|
@ -722,7 +723,7 @@ export type Device = Readonly<{
|
|||
isTouchScreen: boolean;
|
||||
}>;
|
||||
|
||||
type FrameNameBounds = {
|
||||
export type FrameNameBounds = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue