Merge branch 'master' into mtolmacs/fix/small-elbow-routing

This commit is contained in:
Márk Tolmács 2025-04-14 18:54:01 +02:00 committed by GitHub
commit 6dfa5de66c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 161 additions and 34 deletions

View file

@ -112,6 +112,7 @@ export const YOUTUBE_STATES = {
export const ENV = { export const ENV = {
TEST: "test", TEST: "test",
DEVELOPMENT: "development", DEVELOPMENT: "development",
PRODUCTION: "production",
}; };
export const CLASSES = { export const CLASSES = {

View file

@ -739,6 +739,8 @@ export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT; export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
export const isServerEnv = () => export const isServerEnv = () =>
typeof process !== "undefined" && !!process?.env?.NODE_ENV; typeof process !== "undefined" && !!process?.env?.NODE_ENV;

View file

@ -6,6 +6,8 @@ import {
TEXT_ALIGN, TEXT_ALIGN,
VERTICAL_ALIGN, VERTICAL_ALIGN,
getFontString, getFontString,
isProdEnv,
invariant,
} from "@excalidraw/common"; } from "@excalidraw/common";
import type { AppState } from "@excalidraw/excalidraw/types"; import type { AppState } from "@excalidraw/excalidraw/types";
@ -26,6 +28,8 @@ import {
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import type { Radians } from "../../math/src";
import type { MaybeTransformHandleType } from "./transformHandles"; import type { MaybeTransformHandleType } from "./transformHandles";
import type { import type {
ElementsMap, ElementsMap,
@ -44,13 +48,25 @@ export const redrawTextBoundingBox = (
informMutation = true, informMutation = true,
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
if (!isProdEnv()) {
invariant(
!container || !isArrowElement(container) || textElement.angle === 0,
"text element angle must be 0 if bound to arrow container",
);
}
const boundTextUpdates = { const boundTextUpdates = {
x: textElement.x, x: textElement.x,
y: textElement.y, y: textElement.y,
text: textElement.text, text: textElement.text,
width: textElement.width, width: textElement.width,
height: textElement.height, height: textElement.height,
angle: container?.angle ?? textElement.angle, angle: (container
? isArrowElement(container)
? 0
: container.angle
: textElement.angle) as Radians,
}; };
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
@ -335,7 +351,10 @@ export const getTextElementAngle = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null, container: ExcalidrawTextContainer | null,
) => { ) => {
if (!container || isArrowElement(container)) { if (isArrowElement(container)) {
return 0;
}
if (!container) {
return textElement.angle; return textElement.angle;
} }
return container.angle; return container.angle;

View file

@ -21,6 +21,7 @@ import {
import { import {
hasBoundTextElement, hasBoundTextElement,
isArrowElement,
isTextBindableContainer, isTextBindableContainer,
isTextElement, isTextElement,
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
@ -46,6 +47,8 @@ import { CaptureUpdateAction } from "../store";
import { register } from "./register"; import { register } from "./register";
import type { Radians } from "../../math/src";
import type { AppState } from "../types"; import type { AppState } from "../types";
export const actionUnbindText = register({ export const actionUnbindText = register({
@ -155,6 +158,7 @@ export const actionBindText = register({
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER, textAlign: TEXT_ALIGN.CENTER,
autoResize: true, autoResize: true,
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
}); });
mutateElement(container, { mutateElement(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({

View file

@ -5352,37 +5352,37 @@ class App extends React.Component<AppProps, AppState> {
y: sceneY, y: sceneY,
}); });
const element = existingTextElement const element =
? existingTextElement existingTextElement ||
: newTextElement({ newTextElement({
x: parentCenterPosition x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX,
? parentCenterPosition.elementCenterX y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY,
: sceneX, strokeColor: this.state.currentItemStrokeColor,
y: parentCenterPosition backgroundColor: this.state.currentItemBackgroundColor,
? parentCenterPosition.elementCenterY fillStyle: this.state.currentItemFillStyle,
: sceneY, strokeWidth: this.state.currentItemStrokeWidth,
strokeColor: this.state.currentItemStrokeColor, strokeStyle: this.state.currentItemStrokeStyle,
backgroundColor: this.state.currentItemBackgroundColor, roughness: this.state.currentItemRoughness,
fillStyle: this.state.currentItemFillStyle, opacity: this.state.currentItemOpacity,
strokeWidth: this.state.currentItemStrokeWidth, text: "",
strokeStyle: this.state.currentItemStrokeStyle, fontSize,
roughness: this.state.currentItemRoughness, fontFamily,
opacity: this.state.currentItemOpacity, textAlign: parentCenterPosition
text: "", ? "center"
fontSize, : this.state.currentItemTextAlign,
fontFamily, verticalAlign: parentCenterPosition
textAlign: parentCenterPosition ? VERTICAL_ALIGN.MIDDLE
? "center" : DEFAULT_VERTICAL_ALIGN,
: this.state.currentItemTextAlign, containerId: shouldBindToContainer ? container?.id : undefined,
verticalAlign: parentCenterPosition groupIds: container?.groupIds ?? [],
? VERTICAL_ALIGN.MIDDLE lineHeight,
: DEFAULT_VERTICAL_ALIGN, angle: container
containerId: shouldBindToContainer ? container?.id : undefined, ? isArrowElement(container)
groupIds: container?.groupIds ?? [], ? (0 as Radians)
lineHeight, : container.angle
angle: container?.angle ?? (0 as Radians), : (0 as Radians),
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
}); });
if (!existingTextElement && shouldBindToContainer && container) { if (!existingTextElement && shouldBindToContainer && container) {
mutateElement(container, { mutateElement(container, {

View file

@ -439,7 +439,7 @@ const repairContainerElement = (
// if defined, lest boundElements is stale // if defined, lest boundElements is stale
!boundElement.containerId !boundElement.containerId
) { ) {
(boundElement as Mutable<ExcalidrawTextElement>).containerId = (boundElement as Mutable<typeof boundElement>).containerId =
container.id; container.id;
} }
} }
@ -464,6 +464,10 @@ const repairBoundElement = (
? elementsMap.get(boundElement.containerId) ? elementsMap.get(boundElement.containerId)
: null; : null;
(boundElement as Mutable<typeof boundElement>).angle = (
isArrowElement(container) ? 0 : container?.angle ?? 0
) as Radians;
if (!container) { if (!container) {
boundElement.containerId = null; boundElement.containerId = null;
return; return;

View file

@ -31,6 +31,7 @@ import {
mockBoundingClientRect, mockBoundingClientRect,
restoreOriginalGetBoundingClientRect, restoreOriginalGetBoundingClientRect,
} from "../tests/test-utils"; } from "../tests/test-utils";
import { actionBindText } from "../actions";
unmountComponent(); unmountComponent();
@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(null); expect(text.containerId).toBe(null);
expect(text.text).toBe("Excalidraw"); expect(text.text).toBe("Excalidraw");
}); });
it("should reset the text element angle to the container's when binding to rotated non-arrow container", async () => {
const text = API.createElement({
type: "text",
text: "Hello World!",
angle: 45,
});
const rectangle = API.createElement({
type: "rectangle",
width: 90,
height: 75,
angle: 30,
});
API.setElements([rectangle, text]);
API.setSelectedElements([rectangle, text]);
h.app.actionManager.executeAction(actionBindText);
expect(text.angle).toBe(30);
expect(rectangle.angle).toBe(30);
});
it("should reset the text element angle to 0 when binding to rotated arrow container", async () => {
const text = API.createElement({
type: "text",
text: "Hello World!",
angle: 45,
});
const arrow = API.createElement({
type: "arrow",
width: 90,
height: 75,
angle: 30,
});
API.setElements([arrow, text]);
API.setSelectedElements([arrow, text]);
h.app.actionManager.executeAction(actionBindText);
expect(text.angle).toBe(0);
expect(arrow.angle).toBe(30);
});
it("should keep the text label at 0 degrees when used as an arrow label", async () => {
const arrow = API.createElement({
type: "arrow",
width: 90,
height: 75,
angle: 30,
});
API.setElements([arrow]);
API.setSelectedElements([arrow]);
mouse.doubleClickAt(
arrow.x + arrow.width / 2,
arrow.y + arrow.height / 2,
);
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
Keyboard.exitTextEditor(editor);
expect(h.elements[1].angle).toBe(0);
});
it("should keep the text label at the same degrees when used as a non-arrow label", async () => {
const rectangle = API.createElement({
type: "rectangle",
width: 90,
height: 75,
angle: 30,
});
API.setElements([rectangle]);
API.setSelectedElements([rectangle]);
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
Keyboard.exitTextEditor(editor);
expect(h.elements[1].angle).toBe(30);
});
}); });
}); });