Compare commits
7 Commits
master
...
mtolmacs/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a29199ee8d | ||
![]() |
b2b6d69477 | ||
![]() |
001df5bade | ||
![]() |
14d512f321 | ||
![]() |
41c036e1a5 | ||
![]() |
91d36e9b81 | ||
![]() |
27522110df |
@ -1,3 +1,5 @@
|
||||
MODE="development"
|
||||
|
||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
MODE="production"
|
||||
|
||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
|
||||
|
@ -34,6 +34,9 @@
|
||||
<a href="https://discord.gg/UexuTaE">
|
||||
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||
</a>
|
||||
<a href="https://deepwiki.com/excalidraw/excalidraw">
|
||||
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" />
|
||||
</a>
|
||||
<a href="https://twitter.com/excalidraw">
|
||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||
</a>
|
||||
|
@ -974,6 +974,25 @@ export const updateElbowArrowPoints = (
|
||||
),
|
||||
"Elbow arrow segments must be either horizontal or vertical",
|
||||
);
|
||||
|
||||
invariant(
|
||||
updates.fixedSegments?.find(
|
||||
(segment) =>
|
||||
segment.index === 1 &&
|
||||
pointsEqual(segment.start, (updates.points ?? arrow.points)[0]),
|
||||
) == null &&
|
||||
updates.fixedSegments?.find(
|
||||
(segment) =>
|
||||
segment.index === (updates.points ?? arrow.points).length - 1 &&
|
||||
pointsEqual(
|
||||
segment.end,
|
||||
(updates.points ?? arrow.points)[
|
||||
(updates.points ?? arrow.points).length - 1
|
||||
],
|
||||
),
|
||||
) == null,
|
||||
"The first and last segments cannot be fixed",
|
||||
);
|
||||
}
|
||||
|
||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||
|
@ -117,6 +117,7 @@ export class LinearElementEditor {
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly elbowed: boolean;
|
||||
public readonly customLineAngle: number | null;
|
||||
|
||||
constructor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
@ -150,6 +151,7 @@ export class LinearElementEditor {
|
||||
this.hoverPointIndex = -1;
|
||||
this.segmentMidPointHoveredCoords = null;
|
||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||
this.customLineAngle = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -253,6 +255,7 @@ export class LinearElementEditor {
|
||||
const { elementId } = linearElementEditor;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
let customLineAngle = linearElementEditor.customLineAngle;
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
@ -293,6 +296,12 @@ export class LinearElementEditor {
|
||||
const selectedIndex = selectedPointsIndices[0];
|
||||
const referencePoint =
|
||||
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||
customLineAngle =
|
||||
linearElementEditor.customLineAngle ??
|
||||
Math.atan2(
|
||||
element.points[selectedIndex][1] - referencePoint[1],
|
||||
element.points[selectedIndex][0] - referencePoint[0],
|
||||
);
|
||||
|
||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||
element,
|
||||
@ -300,6 +309,7 @@ export class LinearElementEditor {
|
||||
referencePoint,
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
customLineAngle,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
@ -421,6 +431,7 @@ export class LinearElementEditor {
|
||||
? lastClickedPoint
|
||||
: -1,
|
||||
isDragging: true,
|
||||
customLineAngle,
|
||||
};
|
||||
}
|
||||
|
||||
@ -524,6 +535,7 @@ export class LinearElementEditor {
|
||||
: selectedPointsIndices,
|
||||
isDragging: false,
|
||||
pointerOffset: { x: 0, y: 0 },
|
||||
customLineAngle: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1531,6 +1543,7 @@ export class LinearElementEditor {
|
||||
referencePoint: LocalPoint,
|
||||
scenePointer: GlobalPoint,
|
||||
gridSize: NullableGridSize,
|
||||
customLineAngle?: number,
|
||||
) {
|
||||
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
@ -1556,6 +1569,7 @@ export class LinearElementEditor {
|
||||
referencePointCoords[1],
|
||||
gridX,
|
||||
gridY,
|
||||
customLineAngle,
|
||||
);
|
||||
|
||||
return pointRotateRads(
|
||||
|
@ -2,6 +2,12 @@ import {
|
||||
SHIFT_LOCKING_ANGLE,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
normalizeRadians,
|
||||
radiansBetweenAngles,
|
||||
radiansDifference,
|
||||
type Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||
|
||||
@ -134,13 +140,42 @@ export const getLockedLinearCursorAlignSize = (
|
||||
originY: number,
|
||||
x: number,
|
||||
y: number,
|
||||
customAngle?: number,
|
||||
) => {
|
||||
let width = x - originX;
|
||||
let height = y - originY;
|
||||
|
||||
const lockedAngle =
|
||||
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE;
|
||||
const angle = Math.atan2(height, width) as Radians;
|
||||
let lockedAngle = (Math.round(angle / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE) as Radians;
|
||||
|
||||
if (customAngle) {
|
||||
// If custom angle is provided, we check if the angle is close to the
|
||||
// custom angle, snap to that if close engough, otherwise snap to the
|
||||
// higher or lower angle depending on the current angle vs custom angle.
|
||||
const lower = (Math.floor(customAngle / SHIFT_LOCKING_ANGLE) *
|
||||
SHIFT_LOCKING_ANGLE) as Radians;
|
||||
if (
|
||||
radiansBetweenAngles(
|
||||
angle,
|
||||
lower,
|
||||
(lower + SHIFT_LOCKING_ANGLE) as Radians,
|
||||
)
|
||||
) {
|
||||
if (
|
||||
radiansDifference(angle, customAngle as Radians) <
|
||||
SHIFT_LOCKING_ANGLE / 6
|
||||
) {
|
||||
lockedAngle = customAngle as Radians;
|
||||
} else if (
|
||||
normalizeRadians(angle) > normalizeRadians(customAngle as Radians)
|
||||
) {
|
||||
lockedAngle = (lower + SHIFT_LOCKING_ANGLE) as Radians;
|
||||
} else {
|
||||
lockedAngle = lower;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lockedAngle === 0) {
|
||||
height = 0;
|
||||
|
@ -1411,5 +1411,55 @@ describe("Test Linear Elements", () => {
|
||||
expect(line.points[line.points.length - 1][0]).toBe(20);
|
||||
expect(line.points[line.points.length - 1][1]).toBe(-20);
|
||||
});
|
||||
|
||||
it("should preserve original angle when dragging endpoint with SHIFT key", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
enterLineEditingMode(line);
|
||||
|
||||
const elementsMap = arrayToMap(h.elements);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Calculate original angle between first and last point
|
||||
const originalAngle = Math.atan2(
|
||||
points[1][1] - points[0][1],
|
||||
points[1][0] - points[0][0],
|
||||
);
|
||||
|
||||
// Drag the second point (endpoint) with SHIFT key pressed
|
||||
const startPoint = pointFrom<GlobalPoint>(points[1][0], points[1][1]);
|
||||
const endPoint = pointFrom<GlobalPoint>(
|
||||
startPoint[0] + 4,
|
||||
startPoint[1] + 4,
|
||||
);
|
||||
|
||||
// Perform drag with SHIFT key modifier
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.downAt(startPoint[0], startPoint[1]);
|
||||
mouse.moveTo(endPoint[0], endPoint[1]);
|
||||
mouse.upAt(endPoint[0], endPoint[1]);
|
||||
});
|
||||
|
||||
// Get updated points after drag
|
||||
const updatedPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
line,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Calculate new angle
|
||||
const newAngle = Math.atan2(
|
||||
updatedPoints[1][1] - updatedPoints[0][1],
|
||||
updatedPoints[1][0] - updatedPoints[0][0],
|
||||
);
|
||||
|
||||
// The angle should be preserved (within a small tolerance for floating point precision)
|
||||
const angleDifference = Math.abs(newAngle - originalAngle);
|
||||
const tolerance = 0.01; // Small tolerance for floating point precision
|
||||
|
||||
expect(angleDifference).toBeLessThan(tolerance);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1483,13 +1483,13 @@ const getArrowheadOptions = (flip: boolean) => {
|
||||
value: "crowfoot_one",
|
||||
text: t("labels.arrowhead_crowfoot_one"),
|
||||
icon: <ArrowheadCrowfootOneIcon flip={flip} />,
|
||||
keyBinding: "c",
|
||||
keyBinding: "x",
|
||||
},
|
||||
{
|
||||
value: "crowfoot_many",
|
||||
text: t("labels.arrowhead_crowfoot_many"),
|
||||
icon: <ArrowheadCrowfootIcon flip={flip} />,
|
||||
keyBinding: "x",
|
||||
keyBinding: "c",
|
||||
},
|
||||
{
|
||||
value: "crowfoot_one_or_many",
|
||||
|
@ -564,7 +564,7 @@ export const convertElementTypes = (
|
||||
continue;
|
||||
}
|
||||
const fixedSegments: FixedSegment[] = [];
|
||||
for (let i = 0; i < nextPoints.length - 1; i++) {
|
||||
for (let i = 1; i < nextPoints.length - 2; i++) {
|
||||
fixedSegments.push({
|
||||
start: nextPoints[i],
|
||||
end: nextPoints[i + 1],
|
||||
@ -581,6 +581,7 @@ export const convertElementTypes = (
|
||||
);
|
||||
mutateElement(element, app.scene.getNonDeletedElementsMap(), {
|
||||
...updates,
|
||||
endArrowhead: "arrow",
|
||||
});
|
||||
} else {
|
||||
// if we're converting to non-elbow linear element, check if
|
||||
|
@ -8628,6 +8628,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@ -8851,6 +8852,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@ -9267,6 +9269,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
@ -9670,6 +9673,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"endBindingElement": "keep",
|
||||
|
@ -49,3 +49,35 @@ export function radiansToDegrees(degrees: Radians): Degrees {
|
||||
export function isRightAngleRads(rads: Radians): boolean {
|
||||
return Math.abs(Math.sin(2 * rads)) < PRECISION;
|
||||
}
|
||||
|
||||
export function radiansBetweenAngles(
|
||||
a: Radians,
|
||||
min: Radians,
|
||||
max: Radians,
|
||||
): boolean {
|
||||
a = normalizeRadians(a);
|
||||
min = normalizeRadians(min);
|
||||
max = normalizeRadians(max);
|
||||
|
||||
if (min < max) {
|
||||
return a >= min && a <= max;
|
||||
}
|
||||
|
||||
// The range wraps around the 0 angle
|
||||
return a >= min || a <= max;
|
||||
}
|
||||
|
||||
export function radiansDifference(a: Radians, b: Radians): Radians {
|
||||
a = normalizeRadians(a);
|
||||
b = normalizeRadians(b);
|
||||
|
||||
let diff = a - b;
|
||||
|
||||
if (diff < -Math.PI) {
|
||||
diff = (diff + 2 * Math.PI) as Radians;
|
||||
} else if (diff > Math.PI) {
|
||||
diff = (diff - 2 * Math.PI) as Radians;
|
||||
}
|
||||
|
||||
return Math.abs(diff) as Radians;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user