diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 7e8c49ea1c..7eb36d5d96 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -112,6 +112,7 @@ export const YOUTUBE_STATES = { export const ENV = { TEST: "test", DEVELOPMENT: "development", + PRODUCTION: "production", }; export const CLASSES = { diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 54eaa67cc4..b6e9fdd781 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -739,6 +739,8 @@ export const isTestEnv = () => import.meta.env.MODE === ENV.TEST; export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT; +export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION; + export const isServerEnv = () => typeof process !== "undefined" && !!process?.env?.NODE_ENV; diff --git a/packages/element/src/textElement.ts b/packages/element/src/textElement.ts index ea27c318fd..55c3f692ce 100644 --- a/packages/element/src/textElement.ts +++ b/packages/element/src/textElement.ts @@ -6,6 +6,8 @@ import { TEXT_ALIGN, VERTICAL_ALIGN, getFontString, + isProdEnv, + invariant, } from "@excalidraw/common"; import type { AppState } from "@excalidraw/excalidraw/types"; @@ -26,6 +28,8 @@ import { isTextElement, } from "./typeChecks"; +import type { Radians } from "../../math/src"; + import type { MaybeTransformHandleType } from "./transformHandles"; import type { ElementsMap, @@ -44,13 +48,25 @@ export const redrawTextBoundingBox = ( informMutation = true, ) => { 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 = { x: textElement.x, y: textElement.y, text: textElement.text, width: textElement.width, height: textElement.height, - angle: container?.angle ?? textElement.angle, + angle: (container + ? isArrowElement(container) + ? 0 + : container.angle + : textElement.angle) as Radians, }; boundTextUpdates.text = textElement.text; @@ -335,7 +351,10 @@ export const getTextElementAngle = ( textElement: ExcalidrawTextElement, container: ExcalidrawTextContainer | null, ) => { - if (!container || isArrowElement(container)) { + if (isArrowElement(container)) { + return 0; + } + if (!container) { return textElement.angle; } return container.angle; diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index ae18c0d986..d08ad341ee 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -21,6 +21,7 @@ import { import { hasBoundTextElement, + isArrowElement, isTextBindableContainer, isTextElement, isUsingAdaptiveRadius, @@ -46,6 +47,8 @@ import { CaptureUpdateAction } from "../store"; import { register } from "./register"; +import type { Radians } from "../../math/src"; + import type { AppState } from "../types"; export const actionUnbindText = register({ @@ -155,6 +158,7 @@ export const actionBindText = register({ verticalAlign: VERTICAL_ALIGN.MIDDLE, textAlign: TEXT_ALIGN.CENTER, autoResize: true, + angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians, }); mutateElement(container, { boundElements: (container.boundElements || []).concat({ diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 276cde0274..976abfd765 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5352,37 +5352,37 @@ class App extends React.Component { y: sceneY, }); - const element = existingTextElement - ? existingTextElement - : newTextElement({ - x: parentCenterPosition - ? parentCenterPosition.elementCenterX - : sceneX, - y: parentCenterPosition - ? parentCenterPosition.elementCenterY - : sceneY, - strokeColor: this.state.currentItemStrokeColor, - backgroundColor: this.state.currentItemBackgroundColor, - fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, - strokeStyle: this.state.currentItemStrokeStyle, - roughness: this.state.currentItemRoughness, - opacity: this.state.currentItemOpacity, - text: "", - fontSize, - fontFamily, - textAlign: parentCenterPosition - ? "center" - : this.state.currentItemTextAlign, - verticalAlign: parentCenterPosition - ? VERTICAL_ALIGN.MIDDLE - : DEFAULT_VERTICAL_ALIGN, - containerId: shouldBindToContainer ? container?.id : undefined, - groupIds: container?.groupIds ?? [], - lineHeight, - angle: container?.angle ?? (0 as Radians), - frameId: topLayerFrame ? topLayerFrame.id : null, - }); + const element = + existingTextElement || + newTextElement({ + x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX, + y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY, + strokeColor: this.state.currentItemStrokeColor, + backgroundColor: this.state.currentItemBackgroundColor, + fillStyle: this.state.currentItemFillStyle, + strokeWidth: this.state.currentItemStrokeWidth, + strokeStyle: this.state.currentItemStrokeStyle, + roughness: this.state.currentItemRoughness, + opacity: this.state.currentItemOpacity, + text: "", + fontSize, + fontFamily, + textAlign: parentCenterPosition + ? "center" + : this.state.currentItemTextAlign, + verticalAlign: parentCenterPosition + ? VERTICAL_ALIGN.MIDDLE + : DEFAULT_VERTICAL_ALIGN, + containerId: shouldBindToContainer ? container?.id : undefined, + groupIds: container?.groupIds ?? [], + lineHeight, + angle: container + ? isArrowElement(container) + ? (0 as Radians) + : container.angle + : (0 as Radians), + frameId: topLayerFrame ? topLayerFrame.id : null, + }); if (!existingTextElement && shouldBindToContainer && container) { mutateElement(container, { diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 4f050c922e..1811cbb57c 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -439,7 +439,7 @@ const repairContainerElement = ( // if defined, lest boundElements is stale !boundElement.containerId ) { - (boundElement as Mutable).containerId = + (boundElement as Mutable).containerId = container.id; } } @@ -464,6 +464,10 @@ const repairBoundElement = ( ? elementsMap.get(boundElement.containerId) : null; + (boundElement as Mutable).angle = ( + isArrowElement(container) ? 0 : container?.angle ?? 0 + ) as Radians; + if (!container) { boundElement.containerId = null; return; diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index 959c5a0129..0ba1960d6a 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -31,6 +31,7 @@ import { mockBoundingClientRect, restoreOriginalGetBoundingClientRect, } from "../tests/test-utils"; +import { actionBindText } from "../actions"; unmountComponent(); @@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => { expect(text.containerId).toBe(null); 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); + }); }); });