mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Elbow arrow segment fixing & positioning (#8952)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
8551823da9
commit
91ebf8b0ea
33 changed files with 3282 additions and 1716 deletions
|
@ -18,14 +18,12 @@ import {
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool } from "../utils";
|
||||||
import { TrashIcon } from "../components/icons";
|
import { TrashIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
|
||||||
|
|
||||||
const deleteSelectedElements = (
|
const deleteSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
||||||
const framesToBeDeleted = new Set(
|
const framesToBeDeleted = new Set(
|
||||||
getSelectedElements(
|
getSelectedElements(
|
||||||
elements.filter((el) => isFrameLikeElement(el)),
|
elements.filter((el) => isFrameLikeElement(el)),
|
||||||
|
@ -51,7 +49,7 @@ const deleteSelectedElements = (
|
||||||
endBinding:
|
endBinding:
|
||||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||||
});
|
});
|
||||||
mutateElbowArrow(bound, elementsMap, bound.points);
|
mutateElement(bound, { points: bound.points });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -208,12 +206,7 @@ export const actionDeleteSelected = register({
|
||||||
: endBindingElement,
|
: endBindingElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(
|
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
||||||
element,
|
|
||||||
selectedPointsIndices,
|
|
||||||
elementsMap,
|
|
||||||
appState.zoom,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
|
|
|
@ -49,12 +49,13 @@ describe("flipping re-centers selection", () => {
|
||||||
},
|
},
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
|
fixedSegments: null,
|
||||||
points: [
|
points: [
|
||||||
pointFrom(0, 0),
|
pointFrom(0, 0),
|
||||||
pointFrom(0, -35),
|
pointFrom(0, -35),
|
||||||
pointFrom(-90.9, -35),
|
pointFrom(-90, -35),
|
||||||
pointFrom(-90.9, 204.9),
|
pointFrom(-90, 204),
|
||||||
pointFrom(65.1, 204.9),
|
pointFrom(66, 204),
|
||||||
],
|
],
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
}),
|
}),
|
||||||
|
@ -70,13 +71,13 @@ describe("flipping re-centers selection", () => {
|
||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
const rec1 = h.elements.find((el) => el.id === "rec1");
|
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||||
expect(rec1?.x).toBeCloseTo(100);
|
expect(rec1.x).toBeCloseTo(100, 0);
|
||||||
expect(rec1?.y).toBeCloseTo(100);
|
expect(rec1.y).toBeCloseTo(100, 0);
|
||||||
|
|
||||||
const rec2 = h.elements.find((el) => el.id === "rec2");
|
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||||
expect(rec2?.x).toBeCloseTo(220);
|
expect(rec2.x).toBeCloseTo(220, 0);
|
||||||
expect(rec2?.y).toBeCloseTo(250);
|
expect(rec2.y).toBeCloseTo(250, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { getCommonBoundingBox } from "../element/bounds";
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
|
@ -134,12 +134,24 @@ const flipElements = (
|
||||||
|
|
||||||
const { midX, midY } = getCommonBoundingBox(selectedElements);
|
const { midX, midY } = getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
resizeMultipleElements(selectedElements, elementsMap, "nw", app.scene, {
|
resizeMultipleElements(
|
||||||
|
selectedElements,
|
||||||
|
elementsMap,
|
||||||
|
"nw",
|
||||||
|
app.scene,
|
||||||
|
new Map(
|
||||||
|
Array.from(elementsMap.values()).map((element) => [
|
||||||
|
element.id,
|
||||||
|
deepCopyElement(element),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
{
|
||||||
flipByX: flipDirection === "horizontal",
|
flipByX: flipDirection === "horizontal",
|
||||||
flipByY: flipDirection === "vertical",
|
flipByY: flipDirection === "vertical",
|
||||||
shouldResizeFromCenter: true,
|
shouldResizeFromCenter: true,
|
||||||
shouldMaintainAspectRatio: true,
|
shouldMaintainAspectRatio: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
|
@ -181,16 +193,10 @@ const flipElements = (
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
elbowArrows.forEach((element) =>
|
elbowArrows.forEach((element) =>
|
||||||
mutateElbowArrow(
|
mutateElement(element, {
|
||||||
element,
|
x: element.x + diffX,
|
||||||
elementsMap,
|
y: element.y + diffY,
|
||||||
element.points,
|
}),
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
informMutation: false,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -116,10 +116,9 @@ import {
|
||||||
calculateFixedPointForElbowArrowBinding,
|
calculateFixedPointForElbowArrowBinding,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import type { LocalPoint } from "../../math";
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointFrom, vector } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -1560,8 +1559,7 @@ export const actionChangeArrowType = register({
|
||||||
label: "Change arrow types",
|
label: "Change arrow types",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value, app) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
const newElements = changeProperty(elements, appState, (el) => {
|
||||||
elements: changeProperty(elements, appState, (el) => {
|
|
||||||
if (!isArrowElement(el)) {
|
if (!isArrowElement(el)) {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1601,6 @@ export const actionChangeArrowType = register({
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
const endHoveredElement =
|
const endHoveredElement =
|
||||||
!newElement.endBinding &&
|
!newElement.endBinding &&
|
||||||
|
@ -1612,7 +1609,6 @@ export const actionChangeArrowType = register({
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
const startElement = startHoveredElement
|
const startElement = startHoveredElement
|
||||||
? startHoveredElement
|
? startHoveredElement
|
||||||
|
@ -1652,22 +1648,13 @@ export const actionChangeArrowType = register({
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
endHoveredElement &&
|
endHoveredElement &&
|
||||||
bindLinearElement(
|
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
|
||||||
newElement,
|
|
||||||
endHoveredElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
mutateElbowArrow(
|
mutateElement(newElement, {
|
||||||
newElement,
|
points: [finalStartPoint, finalEndPoint].map(
|
||||||
elementsMap,
|
|
||||||
[finalStartPoint, finalEndPoint].map(
|
|
||||||
(p): LocalPoint =>
|
(p): LocalPoint =>
|
||||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||||
),
|
),
|
||||||
vector(0, 0),
|
|
||||||
{
|
|
||||||
...(startElement && newElement.startBinding
|
...(startElement && newElement.startBinding
|
||||||
? {
|
? {
|
||||||
startBinding: {
|
startBinding: {
|
||||||
|
@ -1696,16 +1683,38 @@ export const actionChangeArrowType = register({
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
});
|
||||||
|
|
||||||
|
LinearElementEditor.updateEditorMidPointsCache(
|
||||||
|
newElement,
|
||||||
|
elementsMap,
|
||||||
|
app.state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}),
|
});
|
||||||
appState: {
|
|
||||||
|
const newState = {
|
||||||
...appState,
|
...appState,
|
||||||
currentItemArrowType: value,
|
currentItemArrowType: value,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Change the arrow type and update any other state settings for
|
||||||
|
// the arrow.
|
||||||
|
const selectedId = appState.selectedLinearElement?.elementId;
|
||||||
|
if (selectedId) {
|
||||||
|
const selected = newElements.find((el) => el.id === selectedId);
|
||||||
|
if (selected) {
|
||||||
|
newState.selectedLinearElement = new LinearElementEditor(
|
||||||
|
selected as ExcalidrawLinearElement,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: newElements,
|
||||||
|
appState: newState,
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -165,6 +165,7 @@ import {
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
|
isBindableElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
@ -189,7 +190,6 @@ import type {
|
||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
|
@ -292,7 +292,6 @@ import {
|
||||||
getDateTime,
|
getDateTime,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
toBrandedType,
|
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
|
@ -443,7 +442,6 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
||||||
import { getVisibleSceneBounds } from "../element/bounds";
|
import { getVisibleSceneBounds } from "../element/bounds";
|
||||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||||
import NewElementCanvas from "./canvases/NewElementCanvas";
|
import NewElementCanvas from "./canvases/NewElementCanvas";
|
||||||
import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
|
|
||||||
import {
|
import {
|
||||||
FlowChartCreator,
|
FlowChartCreator,
|
||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
|
@ -3184,49 +3182,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
let elements = opts.elements.map((el, _, elements) => {
|
const elements = restoreElements(opts.elements, null, undefined);
|
||||||
if (isElbowArrow(el)) {
|
|
||||||
const startEndElements = [
|
|
||||||
el.startBinding &&
|
|
||||||
elements.find((l) => l.id === el.startBinding?.elementId),
|
|
||||||
el.endBinding &&
|
|
||||||
elements.find((l) => l.id === el.endBinding?.elementId),
|
|
||||||
];
|
|
||||||
const startBinding = startEndElements[0] ? el.startBinding : null;
|
|
||||||
const endBinding = startEndElements[1] ? el.endBinding : null;
|
|
||||||
return {
|
|
||||||
...el,
|
|
||||||
...updateElbowArrow(
|
|
||||||
{
|
|
||||||
...el,
|
|
||||||
startBinding,
|
|
||||||
endBinding,
|
|
||||||
},
|
|
||||||
toBrandedType<NonDeletedSceneElementsMap>(
|
|
||||||
new Map(
|
|
||||||
startEndElements
|
|
||||||
.filter((x) => x != null)
|
|
||||||
.map(
|
|
||||||
(el) =>
|
|
||||||
[el!.id, el] as [
|
|
||||||
string,
|
|
||||||
Ordered<NonDeletedExcalidrawElement>,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[el.points[0], el.points[el.points.length - 1]],
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
zoom: this.state.zoom,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
elements = restoreElements(elements, null, undefined);
|
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
|
@ -4377,7 +4333,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: selectedElements,
|
||||||
zoom: this.state.zoom,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5365,6 +5320,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
|
|
||||||
|
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
|
event,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
if (
|
if (
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
@ -5378,6 +5338,64 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
} else if (
|
||||||
|
this.state.selectedLinearElement &&
|
||||||
|
isElbowArrow(selectedElements[0])
|
||||||
|
) {
|
||||||
|
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
{ x: sceneX, y: sceneY },
|
||||||
|
this.state,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const midPoint = hitCoords
|
||||||
|
? LinearElementEditor.getSegmentMidPointIndex(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
this.state,
|
||||||
|
hitCoords,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (midPoint && midPoint > -1) {
|
||||||
|
this.store.shouldCaptureIncrement();
|
||||||
|
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
||||||
|
|
||||||
|
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
|
{
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
segmentMidPointHoveredCoords: null,
|
||||||
|
},
|
||||||
|
{ x: sceneX, y: sceneY },
|
||||||
|
this.state,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
const nextIndex = nextCoords
|
||||||
|
? LinearElementEditor.getSegmentMidPointIndex(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
this.state,
|
||||||
|
nextCoords,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
pointerDownState: {
|
||||||
|
...this.state.selectedLinearElement.pointerDownState,
|
||||||
|
segmentMidpoint: {
|
||||||
|
index: nextIndex,
|
||||||
|
value: hitCoords,
|
||||||
|
added: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
segmentMidPointHoveredCoords: nextCoords,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5388,11 +5406,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
|
|
||||||
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
||||||
event,
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedGroupIds = getSelectedGroupIds(this.state);
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
||||||
|
|
||||||
if (selectedGroupIds.length > 0) {
|
if (selectedGroupIds.length > 0) {
|
||||||
|
@ -5849,26 +5862,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (isPathALoop(points, this.state.zoom.value)) {
|
if (isPathALoop(points, this.state.zoom.value)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
}
|
}
|
||||||
if (isElbowArrow(multiElement)) {
|
|
||||||
mutateElbowArrow(
|
|
||||||
multiElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
[
|
|
||||||
...points.slice(0, -1),
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
lastCommittedX + dxFromLastCommitted,
|
|
||||||
lastCommittedY + dyFromLastCommitted,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
isDragging: true,
|
|
||||||
informMutation: false,
|
|
||||||
zoom: this.state.zoom,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// update last uncommitted point
|
// update last uncommitted point
|
||||||
mutateElement(
|
mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
|
@ -5882,8 +5875,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
{
|
||||||
|
isDragging: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// in this path, we're mutating multiElement to reflect
|
// in this path, we're mutating multiElement to reflect
|
||||||
// how it will be after adding pointer position as the next point
|
// how it will be after adding pointer position as the next point
|
||||||
|
@ -6049,7 +6044,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeEmbeddable: { element: hitElement, state: "hover" },
|
activeEmbeddable: { element: hitElement, state: "hover" },
|
||||||
});
|
});
|
||||||
} else {
|
} else if (!hitElement || !isElbowArrow(hitElement)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
if (this.state.activeEmbeddable?.state === "hover") {
|
if (this.state.activeEmbeddable?.state === "hover") {
|
||||||
this.setState({ activeEmbeddable: null });
|
this.setState({ activeEmbeddable: null });
|
||||||
|
@ -6235,14 +6230,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state,
|
this.state,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
const isHoveringAPointHandle = isElbowArrow(element)
|
||||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
? hoverPointIndex === 0 ||
|
||||||
|
hoverPointIndex === element.points.length - 1
|
||||||
|
: hoverPointIndex >= 0;
|
||||||
|
if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
if (
|
if (
|
||||||
|
// Ebow arrows can only be moved when unconnected
|
||||||
!isElbowArrow(element) ||
|
!isElbowArrow(element) ||
|
||||||
!(element.startBinding || element.endBinding)
|
!(element.startBinding || element.endBinding)
|
||||||
) {
|
) {
|
||||||
|
@ -6972,6 +6971,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (
|
if (
|
||||||
selectedElements.length === 1 &&
|
selectedElements.length === 1 &&
|
||||||
!this.state.editingLinearElement &&
|
!this.state.editingLinearElement &&
|
||||||
|
!isElbowArrow(selectedElements[0]) &&
|
||||||
!(
|
!(
|
||||||
this.state.selectedLinearElement &&
|
this.state.selectedLinearElement &&
|
||||||
this.state.selectedLinearElement.hoverPointIndex !== -1
|
this.state.selectedLinearElement.hoverPointIndex !== -1
|
||||||
|
@ -7673,6 +7673,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
|
||||||
|
fixedSegments:
|
||||||
|
this.state.currentItemArrowType === ARROW_TYPE.elbow
|
||||||
|
? []
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
: newLinearElement({
|
: newLinearElement({
|
||||||
type: elementType,
|
type: elementType,
|
||||||
|
@ -7913,6 +7917,63 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.state.selectedLinearElement &&
|
||||||
|
this.state.selectedLinearElement.elbowed &&
|
||||||
|
this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index
|
||||||
|
) {
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let index =
|
||||||
|
this.state.selectedLinearElement.pointerDownState.segmentMidpoint
|
||||||
|
.index;
|
||||||
|
if (index < 0) {
|
||||||
|
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
|
{
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
segmentMidPointHoveredCoords: null,
|
||||||
|
},
|
||||||
|
{ x: gridX, y: gridY },
|
||||||
|
this.state,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
index = nextCoords
|
||||||
|
? LinearElementEditor.getSegmentMidPointIndex(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
this.state,
|
||||||
|
nextCoords,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = LinearElementEditor.moveFixedSegment(
|
||||||
|
this.state.selectedLinearElement,
|
||||||
|
index,
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
if (this.state.selectedLinearElement) {
|
||||||
|
this.setState({
|
||||||
|
selectedLinearElement: {
|
||||||
|
...this.state.selectedLinearElement,
|
||||||
|
segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
|
||||||
|
pointerDownState: ret.pointerDownState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lastPointerCoords =
|
const lastPointerCoords =
|
||||||
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
||||||
this.lastPointerMoveCoords = pointerCoords;
|
this.lastPointerMoveCoords = pointerCoords;
|
||||||
|
@ -8265,7 +8326,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
// when we're editing the name of a frame, we want the user to be
|
// when we're editing the name of a frame, we want the user to be
|
||||||
// able to select and interact with the text input
|
// able to select and interact with the text input
|
||||||
!this.state.editingFrame &&
|
if (!this.state.editingFrame) {
|
||||||
dragSelectedElements(
|
dragSelectedElements(
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
|
@ -8274,6 +8335,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
snapOffset,
|
snapOffset,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementsAreBeingDragged: true,
|
selectedElementsAreBeingDragged: true,
|
||||||
|
@ -8449,26 +8511,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
} else if (points.length > 1 && isElbowArrow(newElement)) {
|
} else if (
|
||||||
mutateElbowArrow(
|
points.length === 2 ||
|
||||||
newElement,
|
(points.length > 1 && isElbowArrow(newElement))
|
||||||
elementsMap,
|
) {
|
||||||
[...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
|
||||||
vector(0, 0),
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
isDragging: true,
|
|
||||||
informMutation: false,
|
|
||||||
zoom: this.state.zoom,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (points.length === 2) {
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
{ isDragging: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8663,6 +8716,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
selectedElementsAreBeingDragged: false,
|
selectedElementsAreBeingDragged: false,
|
||||||
});
|
});
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
if (
|
||||||
|
pointerDownState.drag.hasOccurred &&
|
||||||
|
pointerDownState.hit?.element?.id
|
||||||
|
) {
|
||||||
|
const element = elementsMap.get(pointerDownState.hit.element.id);
|
||||||
|
if (isBindableElement(element)) {
|
||||||
|
// Renormalize elbow arrows when they are changed via indirect move
|
||||||
|
element.boundElements
|
||||||
|
?.filter((e) => e.type === "arrow")
|
||||||
|
.map((e) => elementsMap.get(e.id))
|
||||||
|
.filter((e) => isElbowArrow(e))
|
||||||
|
.forEach((e) => {
|
||||||
|
!!e && mutateElement(e, {}, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle end of dragging a point of a linear element, might close a loop
|
// Handle end of dragging a point of a linear element, might close a loop
|
||||||
// and sets binding element
|
// and sets binding element
|
||||||
if (this.state.editingLinearElement) {
|
if (this.state.editingLinearElement) {
|
||||||
|
@ -8687,6 +8758,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.state.selectedLinearElement) {
|
} else if (this.state.selectedLinearElement) {
|
||||||
|
// Normalize elbow arrow points, remove close parallel segments
|
||||||
|
if (this.state.selectedLinearElement.elbowed) {
|
||||||
|
const element = LinearElementEditor.getElement(
|
||||||
|
this.state.selectedLinearElement.elementId,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
|
if (element) {
|
||||||
|
mutateElement(element, {}, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pointerDownState.hit?.element?.id !==
|
pointerDownState.hit?.element?.id !==
|
||||||
this.state.selectedLinearElement.elementId
|
this.state.selectedLinearElement.elementId
|
||||||
|
@ -9126,10 +9208,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
||||||
isLinearElement(hitElement)
|
isLinearElement(hitElement)
|
||||||
) {
|
) {
|
||||||
const selectedELements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
// set selectedLinearElement when no other element selected except
|
// set selectedLinearElement when no other element selected except
|
||||||
// the one we've hit
|
// the one we've hit
|
||||||
if (selectedELements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedLinearElement: new LinearElementEditor(hitElement),
|
selectedLinearElement: new LinearElementEditor(hitElement),
|
||||||
});
|
});
|
||||||
|
@ -9337,6 +9419,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
// not elbow midpoint dragged
|
||||||
|
!(hitElement && isElbowArrow(hitElement)) &&
|
||||||
// not dragged
|
// not dragged
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
// not resized
|
// not resized
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -101,23 +102,38 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||||
return DEFAULT_FONT_FAMILY;
|
return DEFAULT_FONT_FAMILY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const repairBinding = (
|
const repairBinding = <T extends ExcalidrawLinearElement>(
|
||||||
element: ExcalidrawLinearElement,
|
element: T,
|
||||||
binding: PointBinding | FixedPointBinding | null,
|
binding: PointBinding | FixedPointBinding | null,
|
||||||
): PointBinding | FixedPointBinding | null => {
|
): T extends ExcalidrawElbowArrowElement
|
||||||
|
? FixedPointBinding | null
|
||||||
|
: PointBinding | FixedPointBinding | null => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const focus = binding.focus || 0;
|
||||||
...binding,
|
|
||||||
focus: binding.focus || 0,
|
if (isElbowArrow(element)) {
|
||||||
...(isElbowArrow(element) && isFixedPointBinding(binding)
|
const fixedPointBinding:
|
||||||
|
| ExcalidrawElbowArrowElement["startBinding"]
|
||||||
|
| ExcalidrawElbowArrowElement["endBinding"] = isFixedPointBinding(binding)
|
||||||
? {
|
? {
|
||||||
|
...binding,
|
||||||
|
focus,
|
||||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||||
}
|
}
|
||||||
: {}),
|
: null;
|
||||||
};
|
|
||||||
|
return fixedPointBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
focus,
|
||||||
|
} as T extends ExcalidrawElbowArrowElement
|
||||||
|
? FixedPointBinding | null
|
||||||
|
: PointBinding | FixedPointBinding | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
|
@ -308,8 +324,7 @@ const restoreElement = (
|
||||||
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
|
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Separate arrow from linear element
|
const base = {
|
||||||
return restoreElementWithProperties(element as ExcalidrawArrowElement, {
|
|
||||||
type: element.type,
|
type: element.type,
|
||||||
startBinding: repairBinding(element, element.startBinding),
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
endBinding: repairBinding(element, element.endBinding),
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
|
@ -321,7 +336,20 @@ const restoreElement = (
|
||||||
y,
|
y,
|
||||||
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
||||||
...getSizeFromPoints(points),
|
...getSizeFromPoints(points),
|
||||||
});
|
} as const;
|
||||||
|
|
||||||
|
// TODO: Separate arrow from linear element
|
||||||
|
return isElbowArrow(element)
|
||||||
|
? restoreElementWithProperties(element as ExcalidrawElbowArrowElement, {
|
||||||
|
...base,
|
||||||
|
elbowed: true,
|
||||||
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
|
fixedSegments: element.fixedSegments,
|
||||||
|
startIsSpecial: element.startIsSpecial,
|
||||||
|
endIsSpecial: element.endIsSpecial,
|
||||||
|
})
|
||||||
|
: restoreElementWithProperties(element as ExcalidrawArrowElement, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
// generic elements
|
// generic elements
|
||||||
|
|
|
@ -623,11 +623,9 @@ export const updateBoundElements = (
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { newSize, simultaneouslyUpdated, changedElements, zoom } =
|
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||||
options ?? {};
|
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
@ -661,7 +659,7 @@ export const updateBoundElements = (
|
||||||
|
|
||||||
// `linearElement` is being moved/scaled already, just update the binding
|
// `linearElement` is being moved/scaled already, just update the binding
|
||||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||||
mutateElement(element, bindings);
|
mutateElement(element, bindings, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -703,23 +701,14 @@ export const updateBoundElements = (
|
||||||
}> => update !== null,
|
}> => update !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, updates, {
|
||||||
element,
|
|
||||||
updates,
|
|
||||||
elementsMap,
|
|
||||||
{
|
|
||||||
...(changedElement.id === element.startBinding?.elementId
|
...(changedElement.id === element.startBinding?.elementId
|
||||||
? { startBinding: bindings.startBinding }
|
? { startBinding: bindings.startBinding }
|
||||||
: {}),
|
: {}),
|
||||||
...(changedElement.id === element.endBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { endBinding: bindings.endBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
});
|
||||||
{
|
|
||||||
changedElements,
|
|
||||||
zoom,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !boundText.isDeleted) {
|
if (boundText && !boundText.isDeleted) {
|
||||||
|
@ -778,9 +767,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointHeading = headingForPointFromElement(bindableElement, aabb, p);
|
return headingForPointFromElement(bindableElement, aabb, p);
|
||||||
|
|
||||||
return pointHeading;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDistanceForBinding = (
|
const getDistanceForBinding = (
|
||||||
|
@ -2283,7 +2270,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGlobalFixedPoints = (
|
export const getGlobalFixedPoints = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): [GlobalPoint, GlobalPoint] => {
|
): [GlobalPoint, GlobalPoint] => {
|
||||||
|
|
|
@ -42,9 +42,20 @@ export const dragSelectedElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = _selectedElements.filter(
|
const selectedElements = _selectedElements.filter((element) => {
|
||||||
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
|
if (isElbowArrow(element) && element.startBinding && element.endBinding) {
|
||||||
|
const startElement = _selectedElements.find(
|
||||||
|
(el) => el.id === element.startBinding?.elementId,
|
||||||
);
|
);
|
||||||
|
const endElement = _selectedElements.find(
|
||||||
|
(el) => el.id === element.endBinding?.elementId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return startElement && endElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// we do not want a frame and its elements to be selected at the same time
|
// we do not want a frame and its elements to be selected at the same time
|
||||||
// but when it happens (due to some bug), we want to avoid updating element
|
// but when it happens (due to some bug), we want to avoid updating element
|
||||||
|
@ -78,10 +89,8 @@ export const dragSelectedElements = (
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||||
if (
|
if (!isArrowElement(element)) {
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
!isArrowElement(element)
|
|
||||||
) {
|
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
element,
|
element,
|
||||||
scene.getNonDeletedElementsMap(),
|
scene.getNonDeletedElementsMap(),
|
||||||
|
@ -89,10 +98,10 @@ export const dragSelectedElements = (
|
||||||
if (textElement) {
|
if (textElement) {
|
||||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,20 +9,121 @@ import {
|
||||||
render,
|
render,
|
||||||
} from "../tests/test-utils";
|
} from "../tests/test-utils";
|
||||||
import { bindLinearElement } from "./binding";
|
import { bindLinearElement } from "./binding";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw, mutateElement } from "../index";
|
||||||
import { mutateElbowArrow } from "./routing";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ARROW_TYPE } from "../constants";
|
import { ARROW_TYPE } from "../constants";
|
||||||
|
import type { LocalPoint } from "../../math";
|
||||||
import { pointFrom } from "../../math";
|
import { pointFrom } from "../../math";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
describe("elbow arrow segment move", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can move the second segment of a fully connected elbow arrow", () => {
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: -100,
|
||||||
|
y: -50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: 200,
|
||||||
|
y: 150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(0, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(100, 100);
|
||||||
|
mouse.down();
|
||||||
|
mouse.moveTo(115, 100);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
|
expect(h.state.selectedElementIds).toEqual({ [arrow.id]: true });
|
||||||
|
expect(arrow.fixedSegments?.length).toBe(1);
|
||||||
|
|
||||||
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[110, 0],
|
||||||
|
[110, 200],
|
||||||
|
[190, 200],
|
||||||
|
]);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(105, 74.275);
|
||||||
|
mouse.doubleClick();
|
||||||
|
|
||||||
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[110, 0],
|
||||||
|
[110, 200],
|
||||||
|
[190, 200],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can move the second segment of an unconnected elbow arrow", () => {
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(0, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(250, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(125, 100);
|
||||||
|
mouse.down();
|
||||||
|
mouse.moveTo(130, 100);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[130, 0],
|
||||||
|
[130, 200],
|
||||||
|
[250, 200],
|
||||||
|
]);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(130, 100);
|
||||||
|
mouse.doubleClick();
|
||||||
|
|
||||||
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
|
[0, 0],
|
||||||
|
[125, 0],
|
||||||
|
[125, 200],
|
||||||
|
[250, 200],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("elbow arrow routing", () => {
|
describe("elbow arrow routing", () => {
|
||||||
it("can properly generate orthogonal arrow points", () => {
|
it("can properly generate orthogonal arrow points", () => {
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
|
@ -31,10 +132,12 @@ describe("elbow arrow routing", () => {
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
mutateElement(arrow, {
|
||||||
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
points: [
|
||||||
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
]);
|
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||||
|
],
|
||||||
|
});
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[0, 100],
|
[0, 100],
|
||||||
|
@ -81,7 +184,9 @@ describe("elbow arrow routing", () => {
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]);
|
mutateElement(arrow, {
|
||||||
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
|
});
|
||||||
|
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
@ -182,8 +287,6 @@ describe("elbow arrow ui", () => {
|
||||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[35, 0],
|
||||||
[35, 90],
|
|
||||||
[35, 90], // Note that coordinates are rounded above!
|
|
||||||
[35, 165],
|
[35, 165],
|
||||||
[103, 165],
|
[103, 165],
|
||||||
]);
|
]);
|
2111
packages/excalidraw/element/elbowArrow.ts
Normal file
2111
packages/excalidraw/element/elbowArrow.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -452,20 +452,12 @@ const createBindingArrow = (
|
||||||
bindingArrow as OrderedExcalidrawElement,
|
bindingArrow as OrderedExcalidrawElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(bindingArrow, [
|
||||||
bindingArrow,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
point: bindingArrow.points[1],
|
point: bindingArrow.points[1],
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
changedElements,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return bindingArrow;
|
return bindingArrow;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
pointScaleFromOrigin,
|
pointScaleFromOrigin,
|
||||||
radiansToDegrees,
|
radiansToDegrees,
|
||||||
triangleIncludesPoint,
|
triangleIncludesPoint,
|
||||||
|
vectorFromPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||||
import type { ExcalidrawBindableElement } from "./types";
|
import type { ExcalidrawBindableElement } from "./types";
|
||||||
|
@ -52,9 +53,24 @@ export const vectorToHeading = (vec: Vector): Heading => {
|
||||||
return HEADING_UP;
|
return HEADING_UP;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const headingForPoint = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
o: P,
|
||||||
|
) => vectorToHeading(vectorFromPoint<P>(p, o));
|
||||||
|
|
||||||
|
export const headingForPointIsHorizontal = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
o: P,
|
||||||
|
) => headingIsHorizontal(headingForPoint<P>(p, o));
|
||||||
|
|
||||||
export const compareHeading = (a: Heading, b: Heading) =>
|
export const compareHeading = (a: Heading, b: Heading) =>
|
||||||
a[0] === b[0] && a[1] === b[1];
|
a[0] === b[0] && a[1] === b[1];
|
||||||
|
|
||||||
|
export const headingIsHorizontal = (a: Heading) =>
|
||||||
|
compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
|
||||||
|
|
||||||
|
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
||||||
|
|
||||||
// Gets the heading for the point by creating a bounding box around the rotated
|
// Gets the heading for the point by creating a bounding box around the rotated
|
||||||
// close fitting bounding box, then creating 4 search cones around the center of
|
// close fitting bounding box, then creating 4 search cones around the center of
|
||||||
// the external bbox.
|
// the external bbox.
|
||||||
|
@ -63,7 +79,7 @@ export const headingForPointFromElement = <
|
||||||
>(
|
>(
|
||||||
element: Readonly<ExcalidrawBindableElement>,
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
aabb: Readonly<Bounds>,
|
aabb: Readonly<Bounds>,
|
||||||
p: Readonly<LocalPoint | GlobalPoint>,
|
p: Readonly<Point>,
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const SEARCH_CONE_MULTIPLIER = 2;
|
const SEARCH_CONE_MULTIPLIER = 2;
|
||||||
|
|
||||||
|
@ -117,14 +133,22 @@ export const headingForPointFromElement = <
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (triangleIncludesPoint([top, right, midPoint] as Triangle<Point>, p)) {
|
if (
|
||||||
|
triangleIncludesPoint<Point>([top, right, midPoint] as Triangle<Point>, p)
|
||||||
|
) {
|
||||||
return headingForDiamond(top, right);
|
return headingForDiamond(top, right);
|
||||||
} else if (
|
} else if (
|
||||||
triangleIncludesPoint([right, bottom, midPoint] as Triangle<Point>, p)
|
triangleIncludesPoint<Point>(
|
||||||
|
[right, bottom, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return headingForDiamond(right, bottom);
|
return headingForDiamond(right, bottom);
|
||||||
} else if (
|
} else if (
|
||||||
triangleIncludesPoint([bottom, left, midPoint] as Triangle<Point>, p)
|
triangleIncludesPoint<Point>(
|
||||||
|
[bottom, left, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return headingForDiamond(bottom, left);
|
return headingForDiamond(bottom, left);
|
||||||
}
|
}
|
||||||
|
@ -153,17 +177,17 @@ export const headingForPointFromElement = <
|
||||||
SEARCH_CONE_MULTIPLIER,
|
SEARCH_CONE_MULTIPLIER,
|
||||||
) as Point;
|
) as Point;
|
||||||
|
|
||||||
return triangleIncludesPoint(
|
return triangleIncludesPoint<Point>(
|
||||||
[topLeft, topRight, midPoint] as Triangle<Point>,
|
[topLeft, topRight, midPoint] as Triangle<Point>,
|
||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
? HEADING_UP
|
? HEADING_UP
|
||||||
: triangleIncludesPoint(
|
: triangleIncludesPoint<Point>(
|
||||||
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
? HEADING_RIGHT
|
? HEADING_RIGHT
|
||||||
: triangleIncludesPoint(
|
: triangleIncludesPoint<Point>(
|
||||||
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||||
p,
|
p,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,10 @@ import type {
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
FixedSegment,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
|
@ -24,6 +25,7 @@ import type {
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
|
Zoom,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ import {
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { invariant, toBrandedType, tupleToCoors } from "../utils";
|
import { invariant, tupleToCoors } from "../utils";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
@ -44,7 +46,6 @@ import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import type { Store } from "../store";
|
import type { Store } from "../store";
|
||||||
import { mutateElbowArrow } from "./routing";
|
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
import {
|
import {
|
||||||
|
@ -56,6 +57,8 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
|
pointTranslate,
|
||||||
|
vectorFromPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import {
|
import {
|
||||||
getBezierCurveLength,
|
getBezierCurveLength,
|
||||||
|
@ -65,6 +68,7 @@ import {
|
||||||
mapIntervalToBezierT,
|
mapIntervalToBezierT,
|
||||||
} from "../shapes";
|
} from "../shapes";
|
||||||
import { getGridPoint } from "../snapping";
|
import { getGridPoint } from "../snapping";
|
||||||
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
@ -144,13 +148,13 @@ export class LinearElementEditor {
|
||||||
* @param id the `elementId` from the instance of this class (so that we can
|
* @param id the `elementId` from the instance of this class (so that we can
|
||||||
* statically guarantee this method returns an ExcalidrawLinearElement)
|
* statically guarantee this method returns an ExcalidrawLinearElement)
|
||||||
*/
|
*/
|
||||||
static getElement(
|
static getElement<T extends ExcalidrawLinearElement>(
|
||||||
id: InstanceType<typeof LinearElementEditor>["elementId"],
|
id: InstanceType<typeof LinearElementEditor>["elementId"],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) {
|
): T | null {
|
||||||
const element = elementsMap.get(id);
|
const element = elementsMap.get(id);
|
||||||
if (element) {
|
if (element) {
|
||||||
return element as NonDeleted<ExcalidrawLinearElement>;
|
return element as NonDeleted<T>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -291,9 +295,7 @@ export class LinearElementEditor {
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, [
|
||||||
element,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
point: pointFrom(
|
point: pointFrom(
|
||||||
|
@ -302,9 +304,7 @@ export class LinearElementEditor {
|
||||||
),
|
),
|
||||||
isDragging: selectedIndex === lastClickedPoint,
|
isDragging: selectedIndex === lastClickedPoint,
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
|
@ -339,7 +339,6 @@ export class LinearElementEditor {
|
||||||
isDragging: pointIndex === lastClickedPoint,
|
isDragging: pointIndex === lastClickedPoint,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,9 +421,7 @@ export class LinearElementEditor {
|
||||||
selectedPoint === element.points.length - 1
|
selectedPoint === element.points.length - 1
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, [
|
||||||
element,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: selectedPoint,
|
index: selectedPoint,
|
||||||
point:
|
point:
|
||||||
|
@ -432,9 +429,7 @@ export class LinearElementEditor {
|
||||||
? element.points[element.points.length - 1]
|
? element.points[element.points.length - 1]
|
||||||
: element.points[0],
|
: element.points[0],
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingElement = isBindingEnabled(appState)
|
const bindingElement = isBindingEnabled(appState)
|
||||||
|
@ -495,6 +490,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||||
if (
|
if (
|
||||||
|
!isElbowArrow(element) &&
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
element.points.length > 2 &&
|
element.points.length > 2 &&
|
||||||
!boundText
|
!boundText
|
||||||
|
@ -533,6 +529,7 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
element.points[index],
|
element.points[index],
|
||||||
element.points[index + 1],
|
element.points[index + 1],
|
||||||
|
index,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -573,19 +570,23 @@ export class LinearElementEditor {
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
);
|
);
|
||||||
if (clickedPointIndex >= 0) {
|
if (!isElbowArrow(element) && clickedPointIndex >= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (points.length >= 3 && !appState.editingLinearElement) {
|
if (
|
||||||
|
points.length >= 3 &&
|
||||||
|
!appState.editingLinearElement &&
|
||||||
|
!isElbowArrow(element)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const threshold =
|
const threshold =
|
||||||
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
(LinearElementEditor.POINT_HANDLE_SIZE + 1) / appState.zoom.value;
|
||||||
|
|
||||||
const existingSegmentMidpointHitCoords =
|
const existingSegmentMidpointHitCoords =
|
||||||
linearElementEditor.segmentMidPointHoveredCoords;
|
linearElementEditor.segmentMidPointHoveredCoords;
|
||||||
|
@ -604,10 +605,11 @@ export class LinearElementEditor {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const midPoints: typeof editorMidPointsCache["points"] =
|
const midPoints: typeof editorMidPointsCache["points"] =
|
||||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||||
|
|
||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (midPoints[index] !== null) {
|
if (midPoints[index] !== null) {
|
||||||
const distance = pointDistance(
|
const distance = pointDistance(
|
||||||
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
midPoints[index]!,
|
||||||
pointFrom(scenePointer.x, scenePointer.y),
|
pointFrom(scenePointer.x, scenePointer.y),
|
||||||
);
|
);
|
||||||
if (distance <= threshold) {
|
if (distance <= threshold) {
|
||||||
|
@ -620,16 +622,25 @@ export class LinearElementEditor {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
static isSegmentTooShort(
|
static isSegmentTooShort<P extends GlobalPoint | LocalPoint>(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startPoint: GlobalPoint | LocalPoint,
|
startPoint: P,
|
||||||
endPoint: GlobalPoint | LocalPoint,
|
endPoint: P,
|
||||||
zoom: AppState["zoom"],
|
index: number,
|
||||||
|
zoom: Zoom,
|
||||||
) {
|
) {
|
||||||
let distance = pointDistance(
|
if (isElbowArrow(element)) {
|
||||||
pointFrom(startPoint[0], startPoint[1]),
|
if (index >= 0 && index < element.points.length) {
|
||||||
pointFrom(endPoint[0], endPoint[1]),
|
return (
|
||||||
|
pointDistance(startPoint, endPoint) * zoom.value <
|
||||||
|
LinearElementEditor.POINT_HANDLE_SIZE / 2
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let distance = pointDistance(startPoint, endPoint);
|
||||||
if (element.points.length > 2 && element.roundness) {
|
if (element.points.length > 2 && element.roundness) {
|
||||||
distance = getBezierCurveLength(element, endPoint);
|
distance = getBezierCurveLength(element, endPoint);
|
||||||
}
|
}
|
||||||
|
@ -748,12 +759,8 @@ export class LinearElementEditor {
|
||||||
segmentMidpoint,
|
segmentMidpoint,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
}
|
} else if (event.altKey && appState.editingLinearElement) {
|
||||||
if (event.altKey && appState.editingLinearElement) {
|
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||||
if (
|
|
||||||
linearElementEditor.lastUncommittedPoint == null &&
|
|
||||||
!isElbowArrow(element)
|
|
||||||
) {
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
|
@ -909,12 +916,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
if (!event.altKey) {
|
if (!event.altKey) {
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(
|
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||||
element,
|
|
||||||
[points.length - 1],
|
|
||||||
elementsMap,
|
|
||||||
app.state.zoom,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -952,23 +954,14 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, [
|
||||||
element,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: newPoint,
|
point: newPoint,
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(
|
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
||||||
element,
|
|
||||||
[{ point: newPoint }],
|
|
||||||
elementsMap,
|
|
||||||
app.state.zoom,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -1197,16 +1190,12 @@ export class LinearElementEditor {
|
||||||
// potentially expanding the bounding box
|
// potentially expanding the bounding box
|
||||||
if (pointAddedToEnd) {
|
if (pointAddedToEnd) {
|
||||||
const lastPoint = element.points[element.points.length - 1];
|
const lastPoint = element.points[element.points.length - 1];
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, [
|
||||||
element,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1221,8 +1210,6 @@ export class LinearElementEditor {
|
||||||
static deletePoints(
|
static deletePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
zoom: AppState["zoom"],
|
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
|
@ -1252,47 +1239,27 @@ export class LinearElementEditor {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||||
element,
|
|
||||||
nextPoints,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { point: LocalPoint }[],
|
targetPoints: { point: LocalPoint }[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
zoom: AppState["zoom"],
|
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
const offsetY = 0;
|
const offsetY = 0;
|
||||||
|
|
||||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||||
element,
|
|
||||||
nextPoints,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
options?: {
|
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
|
||||||
isDragging?: boolean;
|
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
@ -1335,7 +1302,6 @@ export class LinearElementEditor {
|
||||||
nextPoints,
|
nextPoints,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
elementsMap,
|
|
||||||
otherUpdates,
|
otherUpdates,
|
||||||
{
|
{
|
||||||
isDragging: targetPoints.reduce(
|
isDragging: targetPoints.reduce(
|
||||||
|
@ -1343,8 +1309,6 @@ export class LinearElementEditor {
|
||||||
dragging || targetPoint.isDragging === true,
|
dragging || targetPoint.isDragging === true,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
changedElements: options?.changedElements,
|
|
||||||
zoom: options?.zoom,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1451,54 +1415,49 @@ export class LinearElementEditor {
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
zoom?: AppState["zoom"];
|
zoom?: AppState["zoom"];
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
const bindings: {
|
const updates: {
|
||||||
startBinding?: FixedPointBinding | null;
|
startBinding?: FixedPointBinding | null;
|
||||||
endBinding?: FixedPointBinding | null;
|
endBinding?: FixedPointBinding | null;
|
||||||
|
points?: LocalPoint[];
|
||||||
} = {};
|
} = {};
|
||||||
if (otherUpdates?.startBinding !== undefined) {
|
if (otherUpdates?.startBinding !== undefined) {
|
||||||
bindings.startBinding =
|
updates.startBinding =
|
||||||
otherUpdates.startBinding !== null &&
|
otherUpdates.startBinding !== null &&
|
||||||
isFixedPointBinding(otherUpdates.startBinding)
|
isFixedPointBinding(otherUpdates.startBinding)
|
||||||
? otherUpdates.startBinding
|
? otherUpdates.startBinding
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
if (otherUpdates?.endBinding !== undefined) {
|
if (otherUpdates?.endBinding !== undefined) {
|
||||||
bindings.endBinding =
|
updates.endBinding =
|
||||||
otherUpdates.endBinding !== null &&
|
otherUpdates.endBinding !== null &&
|
||||||
isFixedPointBinding(otherUpdates.endBinding)
|
isFixedPointBinding(otherUpdates.endBinding)
|
||||||
? otherUpdates.endBinding
|
? otherUpdates.endBinding
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedElementsMap = options?.changedElements
|
updates.points = Array.from(nextPoints);
|
||||||
? toBrandedType<SceneElementsMap>(
|
updates.points[0] = pointTranslate(
|
||||||
new Map([...elementsMap, ...options.changedElements]),
|
updates.points[0],
|
||||||
)
|
|
||||||
: elementsMap;
|
|
||||||
|
|
||||||
mutateElbowArrow(
|
|
||||||
element,
|
|
||||||
mergedElementsMap,
|
|
||||||
nextPoints,
|
|
||||||
vector(offsetX, offsetY),
|
vector(offsetX, offsetY),
|
||||||
bindings,
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
zoom: options?.zoom,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
updates.points[updates.points.length - 1] = pointTranslate(
|
||||||
|
updates.points[updates.points.length - 1],
|
||||||
|
vector(offsetX, offsetY),
|
||||||
|
);
|
||||||
|
|
||||||
|
mutateElement(element, updates, true, {
|
||||||
|
isDragging: options?.isDragging,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
|
@ -1773,6 +1732,99 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static moveFixedSegment(
|
||||||
|
linearElement: LinearElementEditor,
|
||||||
|
index: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): LinearElementEditor {
|
||||||
|
const element = LinearElementEditor.getElement(
|
||||||
|
linearElement.elementId,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!element || !isElbowArrow(element)) {
|
||||||
|
return linearElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index && index > 0 && index < element.points.length) {
|
||||||
|
const isHorizontal = headingIsHorizontal(
|
||||||
|
vectorToHeading(
|
||||||
|
vectorFromPoint(element.points[index], element.points[index - 1]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fixedSegments = (element.fixedSegments ?? []).reduce(
|
||||||
|
(segments, s) => {
|
||||||
|
segments[s.index] = s;
|
||||||
|
return segments;
|
||||||
|
},
|
||||||
|
{} as Record<number, FixedSegment>,
|
||||||
|
);
|
||||||
|
fixedSegments[index] = {
|
||||||
|
index,
|
||||||
|
start: pointFrom<LocalPoint>(
|
||||||
|
!isHorizontal ? x - element.x : element.points[index - 1][0],
|
||||||
|
isHorizontal ? y - element.y : element.points[index - 1][1],
|
||||||
|
),
|
||||||
|
end: pointFrom<LocalPoint>(
|
||||||
|
!isHorizontal ? x - element.x : element.points[index][0],
|
||||||
|
isHorizontal ? y - element.y : element.points[index][1],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const nextFixedSegments = Object.values(fixedSegments).sort(
|
||||||
|
(a, b) => a.index - b.index,
|
||||||
|
);
|
||||||
|
|
||||||
|
const offset = nextFixedSegments
|
||||||
|
.map((segment) => segment.index)
|
||||||
|
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
fixedSegments: nextFixedSegments,
|
||||||
|
});
|
||||||
|
|
||||||
|
const point = pointFrom<GlobalPoint>(
|
||||||
|
element.x +
|
||||||
|
(element.fixedSegments![offset].start[0] +
|
||||||
|
element.fixedSegments![offset].end[0]) /
|
||||||
|
2,
|
||||||
|
element.y +
|
||||||
|
(element.fixedSegments![offset].start[1] +
|
||||||
|
element.fixedSegments![offset].end[1]) /
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...linearElement,
|
||||||
|
segmentMidPointHoveredCoords: point,
|
||||||
|
pointerDownState: {
|
||||||
|
...linearElement.pointerDownState,
|
||||||
|
segmentMidpoint: {
|
||||||
|
added: false,
|
||||||
|
index: element.fixedSegments![offset].index,
|
||||||
|
value: point,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return linearElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteFixedSegment(
|
||||||
|
element: ExcalidrawElbowArrowElement,
|
||||||
|
index: number,
|
||||||
|
): void {
|
||||||
|
mutateElement(element, {
|
||||||
|
fixedSegments: element.fixedSegments?.filter(
|
||||||
|
(segment) => segment.index !== index,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
mutateElement(element, {}, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeSelectedPoints = (
|
const normalizeSelectedPoints = (
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement, SceneElementsMap } from "./types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp, toBrandedType } from "../utils";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import { isElbowArrow } from "./typeChecks";
|
||||||
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
import type { Radians } from "../../math";
|
||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
|
@ -19,14 +22,49 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
informMutation = true,
|
informMutation = true,
|
||||||
|
options?: {
|
||||||
|
// Currently only for elbow arrows.
|
||||||
|
// If true, the elbow arrow tries to bind to the nearest element. If false
|
||||||
|
// it tries to keep the same bound element, if any.
|
||||||
|
isDragging?: boolean;
|
||||||
|
},
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fileId } = updates as any;
|
const { points, fixedSegments, fileId } = updates as any;
|
||||||
|
|
||||||
if (typeof points !== "undefined") {
|
if (
|
||||||
|
isElbowArrow(element) &&
|
||||||
|
(Object.keys(updates).length === 0 || // normalization case
|
||||||
|
typeof points !== "undefined" || // repositioning
|
||||||
|
typeof fixedSegments !== "undefined") // segment fixing
|
||||||
|
) {
|
||||||
|
const elementsMap = toBrandedType<SceneElementsMap>(
|
||||||
|
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||||
|
);
|
||||||
|
|
||||||
|
updates = {
|
||||||
|
...updates,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
...updateElbowArrowPoints(
|
||||||
|
{
|
||||||
|
...element,
|
||||||
|
x: updates.x || element.x,
|
||||||
|
y: updates.y || element.y,
|
||||||
|
},
|
||||||
|
elementsMap,
|
||||||
|
{
|
||||||
|
fixedSegments,
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDragging: options?.isDragging,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else if (typeof points !== "undefined") {
|
||||||
updates = { ...getSizeFromPoints(points), ...updates };
|
updates = { ...getSizeFromPoints(points), ...updates };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ import type {
|
||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
FixedSegment,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
@ -450,15 +452,34 @@ export const newLinearElement = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newArrowElement = (
|
export const newArrowElement = <T extends boolean>(
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawArrowElement["type"];
|
type: ExcalidrawArrowElement["type"];
|
||||||
startArrowhead?: Arrowhead | null;
|
startArrowhead?: Arrowhead | null;
|
||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawArrowElement["points"];
|
points?: ExcalidrawArrowElement["points"];
|
||||||
elbowed?: boolean;
|
elbowed?: T;
|
||||||
|
fixedSegments?: FixedSegment[] | null;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawArrowElement> => {
|
): T extends true
|
||||||
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
: NonDeleted<ExcalidrawArrowElement> => {
|
||||||
|
if (opts.elbowed) {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
|
||||||
|
points: opts.points || [],
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
startBinding: null,
|
||||||
|
endBinding: null,
|
||||||
|
startArrowhead: opts.startArrowhead || null,
|
||||||
|
endArrowhead: opts.endArrowhead || null,
|
||||||
|
elbowed: true,
|
||||||
|
fixedSegments: opts.fixedSegments || [],
|
||||||
|
startIsSpecial: false,
|
||||||
|
endIsSpecial: false,
|
||||||
|
} as NonDeleted<ExcalidrawElbowArrowElement>;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
|
@ -467,8 +488,10 @@ export const newArrowElement = (
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
endArrowhead: opts.endArrowhead || null,
|
endArrowhead: opts.endArrowhead || null,
|
||||||
elbowed: opts.elbowed || false,
|
elbowed: false,
|
||||||
};
|
} as T extends true
|
||||||
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
: NonDeleted<ExcalidrawArrowElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newImageElement = (
|
export const newImageElement = (
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
|
@ -53,7 +54,6 @@ import {
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { isInGroup } from "../groups";
|
import { isInGroup } from "../groups";
|
||||||
import { mutateElbowArrow } from "./routing";
|
|
||||||
import type { GlobalPoint } from "../../math";
|
import type { GlobalPoint } from "../../math";
|
||||||
import {
|
import {
|
||||||
pointCenter,
|
pointCenter,
|
||||||
|
@ -177,10 +177,10 @@ export const transformElements = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
scene,
|
scene,
|
||||||
|
originalElements,
|
||||||
{
|
{
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
originalElementsMap: originalElements,
|
|
||||||
flipByX,
|
flipByX,
|
||||||
flipByY,
|
flipByY,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
|
@ -531,8 +531,10 @@ const rotateMultipleElements = (
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
// Needed to re-route the arrow
|
||||||
mutateElbowArrow(element, elementsMap, points);
|
mutateElement(element, {
|
||||||
|
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
|
@ -1201,6 +1203,7 @@ export const resizeMultipleElements = (
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
originalElementsMap: ElementsMap,
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio = false,
|
shouldMaintainAspectRatio = false,
|
||||||
shouldResizeFromCenter = false,
|
shouldResizeFromCenter = false,
|
||||||
|
@ -1208,7 +1211,6 @@ export const resizeMultipleElements = (
|
||||||
flipByY = false,
|
flipByY = false,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
nextWidth,
|
nextWidth,
|
||||||
originalElementsMap,
|
|
||||||
originalBoundingBox,
|
originalBoundingBox,
|
||||||
}: {
|
}: {
|
||||||
nextWidth?: number;
|
nextWidth?: number;
|
||||||
|
@ -1217,7 +1219,6 @@ export const resizeMultipleElements = (
|
||||||
shouldResizeFromCenter?: boolean;
|
shouldResizeFromCenter?: boolean;
|
||||||
flipByX?: boolean;
|
flipByX?: boolean;
|
||||||
flipByY?: boolean;
|
flipByY?: boolean;
|
||||||
originalElementsMap?: ElementsMap;
|
|
||||||
// added to improve performance
|
// added to improve performance
|
||||||
originalBoundingBox?: BoundingBox;
|
originalBoundingBox?: BoundingBox;
|
||||||
} = {},
|
} = {},
|
||||||
|
@ -1387,6 +1388,9 @@ export const resizeMultipleElements = (
|
||||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
|
startBinding?: ExcalidrawElbowArrowElement["startBinding"];
|
||||||
|
endBinding?: ExcalidrawElbowArrowElement["endBinding"];
|
||||||
|
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"];
|
||||||
};
|
};
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
|
@ -1427,6 +1431,44 @@ export const resizeMultipleElements = (
|
||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isElbowArrow(orig)) {
|
||||||
|
// Mirror fixed point binding for elbow arrows
|
||||||
|
// when resize goes into the negative direction
|
||||||
|
if (orig.startBinding) {
|
||||||
|
update.startBinding = {
|
||||||
|
...orig.startBinding,
|
||||||
|
fixedPoint: [
|
||||||
|
flipByX
|
||||||
|
? -orig.startBinding.fixedPoint[0] + 1
|
||||||
|
: orig.startBinding.fixedPoint[0],
|
||||||
|
flipByY
|
||||||
|
? -orig.startBinding.fixedPoint[1] + 1
|
||||||
|
: orig.startBinding.fixedPoint[1],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (orig.endBinding) {
|
||||||
|
update.endBinding = {
|
||||||
|
...orig.endBinding,
|
||||||
|
fixedPoint: [
|
||||||
|
flipByX
|
||||||
|
? -orig.endBinding.fixedPoint[0] + 1
|
||||||
|
: orig.endBinding.fixedPoint[0],
|
||||||
|
flipByY
|
||||||
|
? -orig.endBinding.fixedPoint[1] + 1
|
||||||
|
: orig.endBinding.fixedPoint[1],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (orig.fixedSegments && rescaledPoints.points) {
|
||||||
|
update.fixedSegments = orig.fixedSegments.map((segment) => ({
|
||||||
|
...segment,
|
||||||
|
start: rescaledPoints.points[segment.index - 1],
|
||||||
|
end: rescaledPoints.points[segment.index],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isImageElement(orig)) {
|
if (isImageElement(orig)) {
|
||||||
update.scale = [
|
update.scale = [
|
||||||
orig.scale[0] * flipFactorX,
|
orig.scale[0] * flipFactorX,
|
||||||
|
@ -1472,7 +1514,10 @@ export const resizeMultipleElements = (
|
||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { width, height, angle } = update;
|
||||||
|
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false, {
|
||||||
|
// needed for the fixed binding point udpate to take effect
|
||||||
|
isDragging: true,
|
||||||
|
});
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -319,6 +319,12 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type FixedSegment = {
|
||||||
|
start: LocalPoint;
|
||||||
|
end: LocalPoint;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "arrow";
|
type: "arrow";
|
||||||
|
@ -331,6 +337,23 @@ export type ExcalidrawElbowArrowElement = Merge<
|
||||||
elbowed: true;
|
elbowed: true;
|
||||||
startBinding: FixedPointBinding | null;
|
startBinding: FixedPointBinding | null;
|
||||||
endBinding: FixedPointBinding | null;
|
endBinding: FixedPointBinding | null;
|
||||||
|
fixedSegments: FixedSegment[] | null;
|
||||||
|
/**
|
||||||
|
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
||||||
|
* order to temporarily hide the first segment of the arrow without losing
|
||||||
|
* the data from the points array. It allows creating the expected arrow
|
||||||
|
* path when the arrow with fixed segments is bound on a horizontal side and
|
||||||
|
* moved to a vertical and vica versa.
|
||||||
|
*/
|
||||||
|
startIsSpecial: boolean | null;
|
||||||
|
/**
|
||||||
|
* Marks that the 3rd point backwards from the end should be used as the 2nd
|
||||||
|
* point of the arrow in order to temporarily hide the last segment of the
|
||||||
|
* arrow without losing the data from the points array. It allows creating
|
||||||
|
* the expected arrow path when the arrow with fixed segments is bound on a
|
||||||
|
* horizontal side and moved to a vertical and vica versa.
|
||||||
|
*/
|
||||||
|
endIsSpecial: boolean | null;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
|
@ -190,7 +190,6 @@ export const syncInvalidIndices = (
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, update] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
"fonteditor-core": "2.4.1",
|
"fonteditor-core": "2.4.1",
|
||||||
"harfbuzzjs": "0.3.6",
|
"harfbuzzjs": "0.3.6",
|
||||||
"import-meta-loader": "1.1.0",
|
"import-meta-loader": "1.1.0",
|
||||||
|
"jest-diff": "29.7.0",
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
getOmitSidesForDevice,
|
getOmitSidesForDevice,
|
||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import { arrayToMap, throttleRAF } from "../utils";
|
import { arrayToMap, invariant, throttleRAF } from "../utils";
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
|
@ -78,9 +78,32 @@ import type {
|
||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "../../math";
|
import {
|
||||||
|
pointFrom,
|
||||||
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
|
type Radians,
|
||||||
|
} from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
|
|
||||||
|
const renderElbowArrowMidPointHighlight = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
invariant(appState.selectedLinearElement, "selectedLinearElement is null");
|
||||||
|
|
||||||
|
const { segmentMidPointHoveredCoords } = appState.selectedLinearElement;
|
||||||
|
|
||||||
|
invariant(segmentMidPointHoveredCoords, "midPointCoords is null");
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
|
||||||
|
highlightPoint(segmentMidPointHoveredCoords, context, appState);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
const renderLinearElementPointHighlight = (
|
const renderLinearElementPointHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
|
@ -490,7 +513,7 @@ const renderLinearPointHandles = (
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(appState.scrollX, appState.scrollY);
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
context.lineWidth = 1 / appState.zoom.value;
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
const points: GlobalPoint[] = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
@ -510,45 +533,46 @@ const renderLinearPointHandles = (
|
||||||
renderSingleLinearPoint(context, appState, point, radius, isSelected);
|
renderSingleLinearPoint(context, appState, point, radius, isSelected);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Rendering segment mid points
|
// Rendering segment mid points
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
const fixedSegments =
|
||||||
|
element.fixedSegments?.map((segment) => segment.index) || [];
|
||||||
|
points.slice(0, -1).forEach((p, idx) => {
|
||||||
|
if (
|
||||||
|
!LinearElementEditor.isSegmentTooShort(
|
||||||
|
element,
|
||||||
|
points[idx + 1],
|
||||||
|
points[idx],
|
||||||
|
idx,
|
||||||
|
appState.zoom,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
renderSingleLinearPoint(
|
||||||
|
context,
|
||||||
|
appState,
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
(p[0] + points[idx + 1][0]) / 2,
|
||||||
|
(p[1] + points[idx + 1][1]) / 2,
|
||||||
|
),
|
||||||
|
POINT_HANDLE_SIZE / 2,
|
||||||
|
false,
|
||||||
|
!fixedSegments.includes(idx + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
appState,
|
||||||
).filter((midPoint): midPoint is GlobalPoint => midPoint !== null);
|
).filter(
|
||||||
|
(midPoint, idx, midPoints): midPoint is GlobalPoint =>
|
||||||
|
midPoint !== null &&
|
||||||
|
!(isElbowArrow(element) && (idx === 0 || idx === midPoints.length - 1)),
|
||||||
|
);
|
||||||
|
|
||||||
midPoints.forEach((segmentMidPoint) => {
|
midPoints.forEach((segmentMidPoint) => {
|
||||||
if (
|
if (appState.editingLinearElement || points.length === 2) {
|
||||||
appState?.selectedLinearElement?.segmentMidPointHoveredCoords &&
|
|
||||||
LinearElementEditor.arePointsEqual(
|
|
||||||
segmentMidPoint,
|
|
||||||
appState.selectedLinearElement.segmentMidPointHoveredCoords,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// The order of renderingSingleLinearPoint and highLight points is different
|
|
||||||
// inside vs outside editor as hover states are different,
|
|
||||||
// in editor when hovered the original point is not visible as hover state fully covers it whereas outside the
|
|
||||||
// editor original point is visible and hover state is just an outer circle.
|
|
||||||
if (appState.editingLinearElement) {
|
|
||||||
renderSingleLinearPoint(
|
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
segmentMidPoint,
|
|
||||||
radius,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
highlightPoint(segmentMidPoint, context, appState);
|
|
||||||
} else {
|
|
||||||
highlightPoint(segmentMidPoint, context, appState);
|
|
||||||
renderSingleLinearPoint(
|
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
segmentMidPoint,
|
|
||||||
radius,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (appState.editingLinearElement || points.length === 2) {
|
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(
|
||||||
context,
|
context,
|
||||||
appState,
|
appState,
|
||||||
|
@ -559,6 +583,7 @@ const renderLinearPointHandles = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
@ -864,6 +889,12 @@ const _renderInteractiveScene = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
isElbowArrow(selectedElements[0]) &&
|
||||||
|
appState.selectedLinearElement &&
|
||||||
|
appState.selectedLinearElement.segmentMidPointHoveredCoords
|
||||||
|
) {
|
||||||
|
renderElbowArrowMidPointHighlight(context, appState);
|
||||||
|
} else if (
|
||||||
appState.selectedLinearElement &&
|
appState.selectedLinearElement &&
|
||||||
appState.selectedLinearElement.hoverPointIndex >= 0 &&
|
appState.selectedLinearElement.hoverPointIndex >= 0 &&
|
||||||
!(
|
!(
|
||||||
|
@ -875,6 +906,7 @@ const _renderInteractiveScene = ({
|
||||||
) {
|
) {
|
||||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint selected elements
|
// Paint selected elements
|
||||||
if (!appState.multiElement && !appState.editingLinearElement) {
|
if (!appState.multiElement && !appState.editingLinearElement) {
|
||||||
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
|
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
|
||||||
|
|
|
@ -23,13 +23,9 @@ import {
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
import type { EmbedsValidationStatus } from "../types";
|
||||||
import {
|
import { pointFrom, pointDistance, type LocalPoint } from "../../math";
|
||||||
pointFrom,
|
|
||||||
pointDistance,
|
|
||||||
type GlobalPoint,
|
|
||||||
type LocalPoint,
|
|
||||||
} from "../../math";
|
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
|
import { headingForPointIsHorizontal } from "../element/heading";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
|
@ -527,45 +523,53 @@ export const _generateElementShape = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateElbowArrowShape = <Point extends GlobalPoint | LocalPoint>(
|
const generateElbowArrowShape = (
|
||||||
points: readonly Point[],
|
points: readonly LocalPoint[],
|
||||||
radius: number,
|
radius: number,
|
||||||
) => {
|
) => {
|
||||||
const subpoints = [] as [number, number][];
|
const subpoints = [] as [number, number][];
|
||||||
for (let i = 1; i < points.length - 1; i += 1) {
|
for (let i = 1; i < points.length - 1; i += 1) {
|
||||||
const prev = points[i - 1];
|
const prev = points[i - 1];
|
||||||
const next = points[i + 1];
|
const next = points[i + 1];
|
||||||
|
const point = points[i];
|
||||||
|
const prevIsHorizontal = headingForPointIsHorizontal(point, prev);
|
||||||
|
const nextIsHorizontal = headingForPointIsHorizontal(next, point);
|
||||||
const corner = Math.min(
|
const corner = Math.min(
|
||||||
radius,
|
radius,
|
||||||
pointDistance(points[i], next) / 2,
|
pointDistance(points[i], next) / 2,
|
||||||
pointDistance(points[i], prev) / 2,
|
pointDistance(points[i], prev) / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (prev[0] < points[i][0] && prev[1] === points[i][1]) {
|
if (prevIsHorizontal) {
|
||||||
|
if (prev[0] < point[0]) {
|
||||||
// LEFT
|
// LEFT
|
||||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||||
} else if (prev[0] === points[i][0] && prev[1] < points[i][1]) {
|
} else {
|
||||||
// UP
|
|
||||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
|
||||||
} else if (prev[0] > points[i][0] && prev[1] === points[i][1]) {
|
|
||||||
// RIGHT
|
// RIGHT
|
||||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||||
|
}
|
||||||
|
} else if (prev[1] < point[1]) {
|
||||||
|
// UP
|
||||||
|
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||||
} else {
|
} else {
|
||||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||||
}
|
}
|
||||||
|
|
||||||
subpoints.push(points[i] as [number, number]);
|
subpoints.push(points[i] as [number, number]);
|
||||||
|
|
||||||
if (next[0] < points[i][0] && next[1] === points[i][1]) {
|
if (nextIsHorizontal) {
|
||||||
|
if (next[0] < point[0]) {
|
||||||
// LEFT
|
// LEFT
|
||||||
subpoints.push([points[i][0] - corner, points[i][1]]);
|
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||||
} else if (next[0] === points[i][0] && next[1] < points[i][1]) {
|
} else {
|
||||||
// UP
|
|
||||||
subpoints.push([points[i][0], points[i][1] - corner]);
|
|
||||||
} else if (next[0] > points[i][0] && next[1] === points[i][1]) {
|
|
||||||
// RIGHT
|
// RIGHT
|
||||||
subpoints.push([points[i][0] + corner, points[i][1]]);
|
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||||
|
}
|
||||||
|
} else if (next[1] < point[1]) {
|
||||||
|
// UP
|
||||||
|
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||||
} else {
|
} else {
|
||||||
|
// DOWN
|
||||||
subpoints.push([points[i][0], points[i][1] + corner]);
|
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10983,7 +10983,9 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
||||||
"focus": "-0.00161",
|
"focus": "-0.00161",
|
||||||
"gap": "3.53708",
|
"gap": "3.53708",
|
||||||
},
|
},
|
||||||
|
"endIsSpecial": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
"fixedSegments": null,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "448.10100",
|
"height": "448.10100",
|
||||||
|
@ -11000,9 +11002,13 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"451.90000",
|
"225.95000",
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"225.95000",
|
||||||
|
"448.10100",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"451.90000",
|
"451.90000",
|
||||||
"448.10100",
|
"448.10100",
|
||||||
|
@ -11022,6 +11028,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
||||||
"focus": "-0.00159",
|
"focus": "-0.00159",
|
||||||
"gap": 5,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
|
"startIsSpecial": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
|
@ -11147,7 +11154,9 @@ History {
|
||||||
"focus": "-0.00161",
|
"focus": "-0.00161",
|
||||||
"gap": "3.53708",
|
"gap": "3.53708",
|
||||||
},
|
},
|
||||||
|
"endIsSpecial": false,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
"fixedSegments": [],
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "236.10000",
|
"height": "236.10000",
|
||||||
|
@ -11185,6 +11194,7 @@ History {
|
||||||
"focus": "-0.00159",
|
"focus": "-0.00159",
|
||||||
"gap": 5,
|
"gap": 5,
|
||||||
},
|
},
|
||||||
|
"startIsSpecial": false,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
|
|
|
@ -171,8 +171,8 @@ describe("Crop an image", () => {
|
||||||
// test corner handle aspect ratio preserving
|
// test corner handle aspect ratio preserving
|
||||||
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
|
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
|
||||||
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
|
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
|
||||||
expect(image.width).toBeLessThanOrEqual(initialWidth);
|
expect(image.width).toBeLessThanOrEqual(initialWidth + 0.0001);
|
||||||
expect(image.height).toBeLessThanOrEqual(initialHeight);
|
expect(image.height).toBeLessThanOrEqual(initialHeight + 0.0001);
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
image = API.createElement({ type: "image", width: 200, height: 100 });
|
image = API.createElement({ type: "image", width: 200, height: 100 });
|
||||||
|
@ -194,7 +194,7 @@ describe("Crop an image", () => {
|
||||||
expect(image.width).toBeCloseTo(image.height);
|
expect(image.width).toBeCloseTo(image.height);
|
||||||
// max height should be reached
|
// max height should be reached
|
||||||
expect(image.height).toBeCloseTo(initialHeight);
|
expect(image.height).toBeCloseTo(initialHeight);
|
||||||
expect(image.width).toBe(initialHeight);
|
expect(image.width).toBeCloseTo(initialHeight);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
FixedSegment,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { newElement, newTextElement, newLinearElement } from "../../element";
|
import { newElement, newTextElement, newLinearElement } from "../../element";
|
||||||
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
||||||
|
@ -197,6 +198,7 @@ export class API {
|
||||||
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
|
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
|
||||||
: never;
|
: never;
|
||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
|
fixedSegments?: FixedSegment[] | null;
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "freedraw"
|
: T extends "freedraw"
|
||||||
|
|
|
@ -2084,7 +2084,8 @@ describe("history", () => {
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
expect(modifiedArrow.points).toEqual([
|
expect(modifiedArrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[451.9000000000001, 0],
|
[225.95000000000005, 0],
|
||||||
|
[225.95000000000005, 448.10100010002003],
|
||||||
[451.9000000000001, 448.10100010002003],
|
[451.9000000000001, 448.10100010002003],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,6 @@ import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontString,
|
FontString,
|
||||||
SceneElementsMap,
|
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw, mutateElement } from "../index";
|
||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
|
@ -1353,9 +1352,7 @@ describe("Test Linear Elements", () => {
|
||||||
const [origStartX, origStartY] = [line.x, line.y];
|
const [origStartX, origStartY] = [line.x, line.y];
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(line, [
|
||||||
line,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||||
|
@ -1367,9 +1364,7 @@ describe("Test Linear Elements", () => {
|
||||||
line.points[line.points.length - 1][1] - 10,
|
line.points[line.points.length - 1][1] - 10,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
new Map() as SceneElementsMap,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
expect(line.x).toBe(origStartX + 10);
|
expect(line.x).toBe(origStartX + 10);
|
||||||
expect(line.y).toBe(origStartY + 10);
|
expect(line.y).toBe(origStartY + 10);
|
||||||
|
|
|
@ -535,7 +535,7 @@ describe("arrow element", () => {
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
||||||
import { getSelectedElements } from "../scene/selection";
|
import { getSelectedElements } from "../scene/selection";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
|
import { diffStringsUnified } from "jest-diff";
|
||||||
|
|
||||||
const customQueries = {
|
const customQueries = {
|
||||||
...queries,
|
...queries,
|
||||||
|
@ -246,6 +247,36 @@ expect.extend({
|
||||||
pass: false,
|
pass: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toCloselyEqualPoints(received, expected, precision) {
|
||||||
|
if (!Array.isArray(received) || !Array.isArray(expected)) {
|
||||||
|
throw new Error("expected and received are not point arrays");
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMPARE = 1 / Math.pow(10, precision || 2);
|
||||||
|
const pass = received.every(
|
||||||
|
(point, idx) =>
|
||||||
|
Math.abs(expected[idx]?.[0] - point[0]) < COMPARE &&
|
||||||
|
Math.abs(expected[idx]?.[1] - point[1]) < COMPARE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pass) {
|
||||||
|
return {
|
||||||
|
message: () => ` The provided array of points are not close enough.
|
||||||
|
|
||||||
|
${diffStringsUnified(
|
||||||
|
JSON.stringify(expected, undefined, 2),
|
||||||
|
JSON.stringify(received, undefined, 2),
|
||||||
|
)}`,
|
||||||
|
pass: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: () => `expected ${received} to not be close to ${expected}`,
|
||||||
|
pass: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
|
type LocalPoint,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
import type { LineSegment } from "../utils";
|
import type { LineSegment } from "../utils";
|
||||||
import type { BoundingBox, Bounds } from "./element/bounds";
|
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||||
|
@ -15,6 +16,8 @@ declare global {
|
||||||
data: DebugElement[][];
|
data: DebugElement[][];
|
||||||
currentFrame?: number;
|
currentFrame?: number;
|
||||||
};
|
};
|
||||||
|
debugDrawPoint: typeof debugDrawPoint;
|
||||||
|
debugDrawLine: typeof debugDrawLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +150,23 @@ export const debugDrawBounds = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const debugDrawPoints = (
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
points,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
points: LocalPoint[];
|
||||||
|
},
|
||||||
|
options?: any,
|
||||||
|
) => {
|
||||||
|
points.forEach((p) =>
|
||||||
|
debugDrawPoint(pointFrom<GlobalPoint>(x + p[0], y + p[1]), options),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const debugCloseFrame = () => {
|
export const debugCloseFrame = () => {
|
||||||
window.visualDebug?.data.push([]);
|
window.visualDebug?.data.push([]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { pointCenter, pointRotateRads } from "./point";
|
import { pointCenter, pointFrom, pointRotateRads } from "./point";
|
||||||
import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
|
import type { GlobalPoint, Line, LocalPoint, Radians } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,8 +38,16 @@ export function lineFromPointArray<P extends GlobalPoint | LocalPoint>(
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the coordinates resulting from rotating the given line about an origin by an angle in degrees
|
/**
|
||||||
// note that when the origin is not given, the midpoint of the given line is used as the origin
|
* Return the coordinates resulting from rotating the given line about an
|
||||||
|
* origin by an angle in degrees note that when the origin is not given,
|
||||||
|
* the midpoint of the given line is used as the origin
|
||||||
|
*
|
||||||
|
* @param l
|
||||||
|
* @param angle
|
||||||
|
* @param origin
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||||
l: Line<Point>,
|
l: Line<Point>,
|
||||||
angle: Radians,
|
angle: Radians,
|
||||||
|
@ -50,3 +58,29 @@ export const lineRotate = <Point extends LocalPoint | GlobalPoint>(
|
||||||
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
pointRotateRads(l[1], origin || pointCenter(l[0], l[1]), angle),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the intersection point (unless the lines are parallel) of two
|
||||||
|
* lines
|
||||||
|
*
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const linesIntersectAt = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
a: Line<Point>,
|
||||||
|
b: Line<Point>,
|
||||||
|
): Point | null => {
|
||||||
|
const A1 = a[1][1] - a[0][1];
|
||||||
|
const B1 = a[0][0] - a[1][0];
|
||||||
|
const A2 = b[1][1] - b[0][1];
|
||||||
|
const B2 = b[0][0] - b[1][0];
|
||||||
|
const D = A1 * B2 - A2 * B1;
|
||||||
|
if (D !== 0) {
|
||||||
|
const C1 = A1 * a[0][0] + B1 * a[0][1];
|
||||||
|
const C2 = A2 * b[0][0] + B2 * b[0][1];
|
||||||
|
return pointFrom<Point>((C1 * B2 - C2 * B1) / D, (A1 * C2 - A2 * C1) / D);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
|
@ -61,6 +61,22 @@ export function pointFromVector<P extends GlobalPoint | LocalPoint>(
|
||||||
return v as unknown as P;
|
return v as unknown as P;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the coordiante object to a point.
|
||||||
|
*
|
||||||
|
* @param coords The coordinate object with x and y properties
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function pointFromCoords<Point extends GlobalPoint | LocalPoint>({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}) {
|
||||||
|
return [x, y] as Point;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the provided value has the shape of a Point.
|
* Checks if the provided value has the shape of a Point.
|
||||||
*
|
*
|
||||||
|
@ -217,7 +233,10 @@ export function pointDistanceSq<P extends LocalPoint | GlobalPoint>(
|
||||||
a: P,
|
a: P,
|
||||||
b: P,
|
b: P,
|
||||||
): number {
|
): number {
|
||||||
return Math.hypot(b[0] - a[0], b[1] - a[1]);
|
const xDiff = b[0] - a[0];
|
||||||
|
const yDiff = b[1] - a[1];
|
||||||
|
|
||||||
|
return xDiff * xDiff + yDiff * yDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -7263,6 +7263,16 @@ jest-canvas-mock@~2.5.2:
|
||||||
cssfontparser "^1.2.1"
|
cssfontparser "^1.2.1"
|
||||||
moo-color "^1.0.2"
|
moo-color "^1.0.2"
|
||||||
|
|
||||||
|
jest-diff@29.7.0, jest-diff@^29.7.0:
|
||||||
|
version "29.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a"
|
||||||
|
integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.0.0"
|
||||||
|
diff-sequences "^29.6.3"
|
||||||
|
jest-get-type "^29.6.3"
|
||||||
|
pretty-format "^29.7.0"
|
||||||
|
|
||||||
jest-diff@^27.0.0:
|
jest-diff@^27.0.0:
|
||||||
version "27.5.1"
|
version "27.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
||||||
|
@ -7273,16 +7283,6 @@ jest-diff@^27.0.0:
|
||||||
jest-get-type "^27.5.1"
|
jest-get-type "^27.5.1"
|
||||||
pretty-format "^27.5.1"
|
pretty-format "^27.5.1"
|
||||||
|
|
||||||
jest-diff@^29.7.0:
|
|
||||||
version "29.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a"
|
|
||||||
integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==
|
|
||||||
dependencies:
|
|
||||||
chalk "^4.0.0"
|
|
||||||
diff-sequences "^29.6.3"
|
|
||||||
jest-get-type "^29.6.3"
|
|
||||||
pretty-format "^29.7.0"
|
|
||||||
|
|
||||||
jest-get-type@^27.5.1:
|
jest-get-type@^27.5.1:
|
||||||
version "27.5.1"
|
version "27.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue