Compare commits

...

3 commits

Author SHA1 Message Date
AnnaVoroshnina
98b496322b
Merge 7b4c4945af into 01304aac49 2025-04-14 17:12:56 +10:00
Márk Tolmács
01304aac49
feat: Keep text label horizontal (#9364)
All checks were successful
Tests / test (push) Successful in 5m5s
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-04-13 21:21:49 +02:00
AnnaVoroshnina
7b4c4945af fix:correction of the Russian translation 2025-04-04 14:53:07 +03:00
8 changed files with 199 additions and 44 deletions

View file

@ -112,6 +112,7 @@ export const YOUTUBE_STATES = {
export const ENV = {
TEST: "test",
DEVELOPMENT: "development",
PRODUCTION: "production",
};
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 isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
export const isServerEnv = () =>
typeof process !== "undefined" && !!process?.env?.NODE_ENV;

View file

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

View file

@ -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({

View file

@ -5352,37 +5352,37 @@ class App extends React.Component<AppProps, AppState> {
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, {

View file

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

View file

@ -138,7 +138,10 @@
"removeAllElementsFromFrame": "",
"eyeDropper": "Взять образец цвета с холста",
"textToDiagram": "Текст в диаграмму",
"prompt": ""
"prompt": "",
"toggleGrid": "Переключить сетку",
"showFonts": "Показать выбор шрифта",
"theme": "Тема"
},
"library": {
"noItems": "Пока ничего не добавлено...",
@ -189,7 +192,7 @@
"couldNotCreateShareableLinkTooBig": "Нельзя создать ссылку, чтобы поделиться. Сцена слишком большая",
"couldNotLoadInvalidFile": "Не удалось загрузить недопустимый файл",
"importBackendFailed": "Не удалось импортировать из бэкэнда.",
"cannotExportEmptyCanvas": "Не может экспортировать пустой холст.",
"cannotExportEmptyCanvas": "Невозможно экспортировать пустой холст.",
"couldNotCopyToClipboard": "Не удалось скопировать в буфер обмена.",
"decryptFailed": "Не удалось расшифровать данные.",
"uploadedSecurly": "Загружаемые данные защищена сквозным шифрованием, что означает, что сервер Excalidraw и третьи стороны не могут прочитать содержимое.",
@ -250,10 +253,10 @@
"eraser": "Ластик",
"frame": "",
"magicframe": "",
"embeddable": "",
"embeddable": "Веб-вставка",
"laser": "Лазерная указка",
"hand": "Рука (перемещение холста)",
"extraTools": "",
"extraTools": "Дополнительные инструменты",
"mermaidToExcalidraw": "Из Mermaid в Excalidraw",
"magicSettings": "Параметры AI"
},
@ -267,7 +270,7 @@
"linearElement": "Нажмите, чтобы начать несколько точек, перетащите для одной линии",
"freeDraw": "Нажмите и перетаскивайте, отпустите по завершении",
"text": "Совет: при выбранном инструменте выделения дважды щёлкните в любом месте, чтобы добавить текст",
"embeddable": "",
"embeddable": "Нажмите и перетащите, чтобы встроить веб-сайт",
"text_selected": "Дважды щелкните мышью или нажмите ENTER, чтобы редактировать текст",
"text_editing": "Нажмите Escape либо Ctrl или Cmd + ENTER для завершения редактирования",
"linearElementMulti": "Кликните на последней точке или нажмите Escape или Enter чтобы закончить",
@ -350,7 +353,11 @@
"zoomToSelection": "Увеличить до выделенного",
"toggleElementLock": "Заблокировать/разблокировать выделение",
"movePageUpDown": "Сдвинуть страницу вверх/вниз",
"movePageLeftRight": "Сдвинуть страницу вправо/влево"
"movePageLeftRight": "Сдвинуть страницу вправо/влево",
"cropStart": "Обрезать изображение",
"cropFinish": "Завершить обрезку изображения",
"createFlowchart": "Создать блок-схему на основе выбранного элемента",
"navigateFlowchart": "Навигация по блок-схеме"
},
"clearCanvasDialog": {
"title": "Очистить холст"
@ -427,12 +434,15 @@
"scene": "Сцены",
"selected": "Выбран",
"storage": "Хранилище",
"title": "Статистика для ботаников",
"title": "Характеристики",
"total": "Всего",
"version": "Версия",
"versionCopy": "Копировать",
"versionNotAvailable": "Версия не доступна",
"width": "Ширина"
"width": "Ширина",
"fullTitle": "Свойства холста и фигур",
"generalStats": "Общие",
"shapes": "Фигуры"
},
"toast": {
"addedToLibrary": "Добавлено в библиотеку",
@ -444,7 +454,7 @@
"canvas": "холст",
"selection": "выделение",
"pasteAsSingleElement": "Используйте {{shortcut}}, чтобы вставить один объект,\nили вставьте в существующий текстовый редактор",
"unableToEmbed": "",
"unableToEmbed": "Встраивание этого URL в данный момент запрещено...",
"unrecognizedLinkFormat": ""
},
"colors": {
@ -518,8 +528,26 @@
"mermaid": {
"title": "Из Mermaid в Excalidraw",
"button": "Вставить",
"description": "",
"description": "В настоящее время поддерживаются только <flowchartLink>блок-схемы</flowchartLink>, <sequenceLink>последовательности</sequenceLink> и <classLink>классы</classLink>. Остальные типы будут отображаться как изображения в Excalidraw.",
"syntax": "Синтаксис Mermaid",
"preview": "Предпросмотр"
},
"fontList": {
"sceneFonts": "В этой сцене",
"availableFonts": "Доступные шрифты",
"empty": "Шрифты не найдены"
},
"quickSearch": {
"placeholder": "Быстрый поиск"
},
"search": {
"title": "Поиск на холсте",
"noMatch": "Совпадений не найдено...",
"singleResult": "результат",
"multipleResults": "результаты",
"placeholder": "Найти текст на холсте..."
},
"commandPalette": {
"title": "Палитра команд"
}
}

View file

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