POC: auto-transform to polygon on bg set
This commit is contained in:
parent
a9a2c953b4
commit
1fdf8967ed
@ -5,6 +5,7 @@ import {
|
|||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
invariant,
|
invariant,
|
||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
|
MIN_LOOP_LOCK_DISTANCE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
@ -39,6 +40,7 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawLineElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@ -396,3 +398,81 @@ export const isPathALoop = (
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleLinePolygonState = (
|
||||||
|
element: ExcalidrawLineElement,
|
||||||
|
nextPolygonState: boolean,
|
||||||
|
) => {
|
||||||
|
const updatedPoints = [...element.points];
|
||||||
|
|
||||||
|
if (nextPolygonState) {
|
||||||
|
const firstPoint = updatedPoints[0];
|
||||||
|
const lastPoint = updatedPoints[updatedPoints.length - 1];
|
||||||
|
|
||||||
|
const distance = Math.hypot(
|
||||||
|
firstPoint[0] - lastPoint[0],
|
||||||
|
firstPoint[1] - lastPoint[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance > MIN_LOOP_LOCK_DISTANCE) {
|
||||||
|
updatedPoints.push(pointFrom(firstPoint[0], firstPoint[1]));
|
||||||
|
} else {
|
||||||
|
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
||||||
|
firstPoint[0],
|
||||||
|
firstPoint[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (element.loopLock) {
|
||||||
|
// When toggling from loopLock=true to loopLock=false
|
||||||
|
// We need to dislocate the end point by 15 points
|
||||||
|
|
||||||
|
// When loopLock is true, the last point is the same as the first point
|
||||||
|
// We'll use the direction from second-to-last point to first point
|
||||||
|
const firstPoint = updatedPoints[0];
|
||||||
|
|
||||||
|
if (updatedPoints.length >= 3) {
|
||||||
|
const secondLastPoint = updatedPoints[updatedPoints.length - 2];
|
||||||
|
|
||||||
|
// Get direction from second-last to first
|
||||||
|
const dx = firstPoint[0] - secondLastPoint[0];
|
||||||
|
const dy = firstPoint[1] - secondLastPoint[1];
|
||||||
|
|
||||||
|
// Calculate perpendicular direction (rotate 90 degrees)
|
||||||
|
// This creates a visible gap perpendicular to the line direction
|
||||||
|
const perpDx = dy;
|
||||||
|
const perpDy = -dx;
|
||||||
|
|
||||||
|
// Normalize the perpendicular direction vector
|
||||||
|
const perpLength = Math.sqrt(perpDx * perpDx + perpDy * perpDy);
|
||||||
|
let normalizedPerpDx = 0;
|
||||||
|
let normalizedPerpDy = 0;
|
||||||
|
|
||||||
|
if (perpLength > 0) {
|
||||||
|
normalizedPerpDx = perpDx / perpLength;
|
||||||
|
normalizedPerpDy = perpDy / perpLength;
|
||||||
|
} else {
|
||||||
|
// Default perpendicular if points are the same
|
||||||
|
normalizedPerpDx = -0.7071;
|
||||||
|
normalizedPerpDy = 0.7071;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the end point perpendicular to the line direction
|
||||||
|
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
||||||
|
firstPoint[0] + normalizedPerpDx * 15,
|
||||||
|
firstPoint[1] + normalizedPerpDy * 15,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For simple lines with fewer than 3 points
|
||||||
|
// Just move away from the first point at a 45-degree angle
|
||||||
|
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
||||||
|
firstPoint[0] + 10.6,
|
||||||
|
firstPoint[1] - 10.6, // Different direction to avoid crossing
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loopLock: nextPolygonState,
|
||||||
|
points: updatedPoints,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -5,9 +5,6 @@ import {
|
|||||||
isLineElement,
|
isLineElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
import { arrayToMap } from "@excalidraw/common";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
import { MIN_LOOP_LOCK_DISTANCE } from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { pointFrom } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -26,6 +23,10 @@ import { CaptureUpdateAction } from "../store";
|
|||||||
|
|
||||||
import { ButtonIcon } from "../components/ButtonIcon";
|
import { ButtonIcon } from "../components/ButtonIcon";
|
||||||
|
|
||||||
|
import { newElementWith } from "../../element/src/mutateElement";
|
||||||
|
|
||||||
|
import { toggleLinePolygonState } from "../../element/src/shapes";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleLinearEditor = register({
|
export const actionToggleLinearEditor = register({
|
||||||
@ -96,85 +97,6 @@ export const actionToggleLinearEditor = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLoopLock = (
|
|
||||||
element: ExcalidrawLineElement,
|
|
||||||
newLoopLockState: boolean,
|
|
||||||
app: any,
|
|
||||||
) => {
|
|
||||||
const updatedPoints = [...element.points];
|
|
||||||
|
|
||||||
if (newLoopLockState) {
|
|
||||||
const firstPoint = updatedPoints[0];
|
|
||||||
const lastPoint = updatedPoints[updatedPoints.length - 1];
|
|
||||||
|
|
||||||
const distance = Math.hypot(
|
|
||||||
firstPoint[0] - lastPoint[0],
|
|
||||||
firstPoint[1] - lastPoint[1],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distance > MIN_LOOP_LOCK_DISTANCE) {
|
|
||||||
updatedPoints.push(pointFrom(firstPoint[0], firstPoint[1]));
|
|
||||||
} else {
|
|
||||||
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
|
||||||
firstPoint[0],
|
|
||||||
firstPoint[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (element.loopLock) {
|
|
||||||
// When toggling from loopLock=true to loopLock=false
|
|
||||||
// We need to dislocate the end point by 15 points
|
|
||||||
|
|
||||||
// When loopLock is true, the last point is the same as the first point
|
|
||||||
// We'll use the direction from second-to-last point to first point
|
|
||||||
const firstPoint = updatedPoints[0];
|
|
||||||
|
|
||||||
if (updatedPoints.length >= 3) {
|
|
||||||
const secondLastPoint = updatedPoints[updatedPoints.length - 2];
|
|
||||||
|
|
||||||
// Get direction from second-last to first
|
|
||||||
const dx = firstPoint[0] - secondLastPoint[0];
|
|
||||||
const dy = firstPoint[1] - secondLastPoint[1];
|
|
||||||
|
|
||||||
// Calculate perpendicular direction (rotate 90 degrees)
|
|
||||||
// This creates a visible gap perpendicular to the line direction
|
|
||||||
const perpDx = dy;
|
|
||||||
const perpDy = -dx;
|
|
||||||
|
|
||||||
// Normalize the perpendicular direction vector
|
|
||||||
const perpLength = Math.sqrt(perpDx * perpDx + perpDy * perpDy);
|
|
||||||
let normalizedPerpDx = 0;
|
|
||||||
let normalizedPerpDy = 0;
|
|
||||||
|
|
||||||
if (perpLength > 0) {
|
|
||||||
normalizedPerpDx = perpDx / perpLength;
|
|
||||||
normalizedPerpDy = perpDy / perpLength;
|
|
||||||
} else {
|
|
||||||
// Default perpendicular if points are the same
|
|
||||||
normalizedPerpDx = -0.7071;
|
|
||||||
normalizedPerpDy = 0.7071;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the end point perpendicular to the line direction
|
|
||||||
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
|
||||||
firstPoint[0] + normalizedPerpDx * 15,
|
|
||||||
firstPoint[1] + normalizedPerpDy * 15,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// For simple lines with fewer than 3 points
|
|
||||||
// Just move away from the first point at a 45-degree angle
|
|
||||||
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
|
||||||
firstPoint[0] + 10.6,
|
|
||||||
firstPoint[1] - 10.6, // Different direction to avoid crossing
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.scene.mutateElement(element, {
|
|
||||||
loopLock: newLoopLockState,
|
|
||||||
points: updatedPoints,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionToggleLoopLock = register({
|
export const actionToggleLoopLock = register({
|
||||||
name: "toggleLoopLock",
|
name: "toggleLoopLock",
|
||||||
category: DEFAULT_CATEGORIES.elements,
|
category: DEFAULT_CATEGORIES.elements,
|
||||||
@ -208,28 +130,33 @@ export const actionToggleLoopLock = register({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
perform(elements, appState, _, app) {
|
perform(elements, appState, _, app) {
|
||||||
const selectedElements = app.scene
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
.getSelectedElements({
|
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
})
|
|
||||||
.filter((element) => isLineElement(element)) as ExcalidrawLineElement[];
|
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
if (selectedElements.some((element) => !isLineElement(element))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetElements = selectedElements as ExcalidrawLineElement[];
|
||||||
|
|
||||||
// Check if we should lock or unlock based on current state
|
// Check if we should lock or unlock based on current state
|
||||||
// If all elements are locked, unlock all. Otherwise, lock all.
|
// If all elements are locked, unlock all. Otherwise, lock all.
|
||||||
const allLocked = selectedElements.every((element) => element.loopLock);
|
const allLocked = targetElements.every((element) => element.loopLock);
|
||||||
const newLoopLockState = !allLocked;
|
const newLoopLockState = !allLocked;
|
||||||
|
|
||||||
selectedElements.forEach((element) => {
|
const targetElementsMap = arrayToMap(targetElements);
|
||||||
updateLoopLock(element, newLoopLockState, app);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (!targetElementsMap.has(element.id) || !isLineElement(element)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(
|
||||||
|
element,
|
||||||
|
toggleLinePolygonState(element, newLoopLockState),
|
||||||
|
);
|
||||||
|
}),
|
||||||
appState,
|
appState,
|
||||||
elements,
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
getLineHeight,
|
getLineHeight,
|
||||||
|
isTransparent,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
@ -46,6 +47,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
isLineElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
@ -133,6 +135,8 @@ import {
|
|||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { toggleLinePolygonState } from "../../element/src/shapes";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { CaptureUpdateActionType } from "../store";
|
import type { CaptureUpdateActionType } from "../store";
|
||||||
@ -346,22 +350,50 @@ export const actionChangeBackgroundColor = register({
|
|||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
label: "labels.changeBackground",
|
label: "labels.changeBackground",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value, app) => {
|
||||||
|
if (!value.currentItemBackgroundColor) {
|
||||||
return {
|
return {
|
||||||
...(value.currentItemBackgroundColor && {
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
const shouldEnablePolygon = selectedElements.every((el) =>
|
||||||
|
isLineElement(el),
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextElements;
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldEnablePolygon &&
|
||||||
|
value.currentItemBackgroundColor &&
|
||||||
|
!isTransparent(value.currentItemBackgroundColor)
|
||||||
|
) {
|
||||||
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
|
nextElements = elements.map((el) => {
|
||||||
|
if (selectedElementsMap.has(el.id) && isLineElement(el)) {
|
||||||
|
return newElementWith(el, {
|
||||||
|
backgroundColor: value.currentItemBackgroundColor,
|
||||||
|
...toggleLinePolygonState(el, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextElements = changeProperty(elements, appState, (el) =>
|
||||||
newElementWith(el, {
|
newElementWith(el, {
|
||||||
backgroundColor: value.currentItemBackgroundColor,
|
backgroundColor: value.currentItemBackgroundColor,
|
||||||
}),
|
}),
|
||||||
),
|
);
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: nextElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...value,
|
...value,
|
||||||
},
|
},
|
||||||
captureUpdate: !!value.currentItemBackgroundColor
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
? CaptureUpdateAction.IMMEDIATELY
|
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user