POC: auto-transform to polygon on bg set

This commit is contained in:
dwelle 2025-05-04 19:20:27 +02:00
parent a9a2c953b4
commit 1fdf8967ed
3 changed files with 143 additions and 104 deletions

View File

@ -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,
};
};

View File

@ -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,
}; };
}, },

View File

@ -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 }) => (