fix: improve text wrapping inside rhombus and more fixes (#6265)

* fix: improve text wrapping inside rhombus

* Add comments

* specs

* fix: shift resize and multiple element regression for ellipse and rhombus

* use container width for scaling font size

* fix

* fix multiple resize

* lint

* redraw on submit

* redraw only newly pasted elements

* no padding when center

* fix tests

* fix

* dont add padding in rhombus when aligning

* refactor

* fix

* move getMaxContainerHeight and getMaxContainerWidth to textElement.ts

* Add specs
This commit is contained in:
Aakansha Doshi 2023-02-22 16:28:12 +05:30 committed by GitHub
parent 88ff32e9b3
commit 5368ddef74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 192 additions and 101 deletions

View File

@ -1627,6 +1627,7 @@ class App extends React.Component<AppProps, AppState> {
oldIdToDuplicatedId.set(element.id, newElement.id); oldIdToDuplicatedId.set(element.id, newElement.id);
return newElement; return newElement;
}); });
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId); bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
const nextElements = [ const nextElements = [
...this.scene.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
@ -1640,10 +1641,10 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements(nextElements); this.scene.replaceAllElements(nextElements);
nextElements.forEach((nextElement) => { newElements.forEach((newElement) => {
if (isTextElement(nextElement) && isBoundToContainer(nextElement)) { if (isTextElement(newElement) && isBoundToContainer(newElement)) {
const container = getContainerElement(nextElement); const container = getContainerElement(newElement);
redrawTextBoundingBox(nextElement, container); redrawTextBoundingBox(newElement, container);
} }
}); });

View File

@ -22,15 +22,15 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math"; import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds"; import { getResizedElementAbsoluteCoords } from "./bounds";
import { import {
getBoundTextElement,
getBoundTextElementOffset, getBoundTextElementOffset,
getContainerDims, getContainerDims,
getContainerElement, getContainerElement,
measureText, measureText,
normalizeText, normalizeText,
wrapText, wrapText,
getMaxContainerWidth,
} from "./textElement"; } from "./textElement";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants"; import { VERTICAL_ALIGN } from "../constants";
import { isArrowElement } from "./typeChecks"; import { isArrowElement } from "./typeChecks";
type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
@ -278,48 +278,6 @@ export const refreshTextDimensions = (
return { text, ...dimensions }; return { text, ...dimensions };
}; };
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
if (containerWidth <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.width;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return containerWidth;
} else if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return height;
} else if (container.type === "ellipse") {
// The height of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
return height - BOUND_TEXT_PADDING * 2;
};
export const updateTextElement = ( export const updateTextElement = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
{ {

View File

@ -43,12 +43,12 @@ import {
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement, getBoundTextElement,
getBoundTextElementId, getBoundTextElementId,
getBoundTextElementOffset,
getContainerElement, getContainerElement,
handleBindTextResize, handleBindTextResize,
measureText, measureText,
getMaxContainerHeight,
getMaxContainerWidth,
} from "./textElement"; } from "./textElement";
import { getMaxContainerWidth } from "./newElement";
export const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) { if (angle >= 2 * Math.PI) {
@ -427,12 +427,16 @@ export const resizeSingleElement = (
}; };
} }
if (shouldMaintainAspectRatio) { if (shouldMaintainAspectRatio) {
const boundTextElementPadding = const updatedElement = {
getBoundTextElementOffset(boundTextElement); ...element,
width: eleNewWidth,
height: eleNewHeight,
};
const nextFont = measureFontSizeFromWH( const nextFont = measureFontSizeFromWH(
boundTextElement, boundTextElement,
eleNewWidth - boundTextElementPadding * 2, getMaxContainerWidth(updatedElement),
eleNewHeight - boundTextElementPadding * 2, getMaxContainerHeight(updatedElement),
); );
if (nextFont === null) { if (nextFont === null) {
return; return;
@ -697,11 +701,15 @@ const resizeMultipleElements = (
const boundTextElement = getBoundTextElement(element.latest); const boundTextElement = getBoundTextElement(element.latest);
if (boundTextElement || isTextElement(element.orig)) { if (boundTextElement || isTextElement(element.orig)) {
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2; const updatedElement = {
...element.latest,
width,
height,
};
const textMeasurements = measureFontSizeFromWH( const textMeasurements = measureFontSizeFromWH(
boundTextElement ?? (element.orig as ExcalidrawTextElement), boundTextElement ?? (element.orig as ExcalidrawTextElement),
width - optionalPadding, getMaxContainerWidth(updatedElement),
height - optionalPadding, getMaxContainerHeight(updatedElement),
); );
if (!textMeasurements) { if (!textMeasurements) {

View File

@ -3,6 +3,8 @@ import { API } from "../tests/helpers/api";
import { import {
computeContainerHeightForBoundText, computeContainerHeightForBoundText,
getContainerCoords, getContainerCoords,
getMaxContainerWidth,
getMaxContainerHeight,
measureText, measureText,
wrapText, wrapText,
} from "./textElement"; } from "./textElement";
@ -202,24 +204,37 @@ describe("Test measureText", () => {
describe("Test getContainerCoords", () => { describe("Test getContainerCoords", () => {
const params = { width: 200, height: 100, x: 10, y: 20 }; const params = { width: 200, height: 100, x: 10, y: 20 };
it("should compute coords correctly when ellipse", () => { it("should compute coords correctly when ellipse", () => {
const ellipse = API.createElement({ const element = API.createElement({
type: "ellipse", type: "ellipse",
...params, ...params,
}); });
expect(getContainerCoords(ellipse)).toEqual({ expect(getContainerCoords(element)).toEqual({
x: 44.2893218813452455, x: 44.2893218813452455,
y: 39.64466094067262, y: 39.64466094067262,
}); });
}); });
it("should compute coords correctly when rectangle", () => { it("should compute coords correctly when rectangle", () => {
const rectangle = API.createElement({ const element = API.createElement({
type: "rectangle", type: "rectangle",
...params, ...params,
}); });
expect(getContainerCoords(rectangle)).toEqual({ expect(getContainerCoords(element)).toEqual({
x: 10, x: 15,
y: 20, y: 25,
});
});
it("should compute coords correctly when diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 65,
y: 50,
}); });
}); });
}); });
@ -229,6 +244,7 @@ describe("Test measureText", () => {
width: 178, width: 178,
height: 194, height: 194,
}; };
it("should compute container height correctly for rectangle", () => { it("should compute container height correctly for rectangle", () => {
const element = API.createElement({ const element = API.createElement({
type: "rectangle", type: "rectangle",
@ -236,6 +252,7 @@ describe("Test measureText", () => {
}); });
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160); expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
}); });
it("should compute container height correctly for ellipse", () => { it("should compute container height correctly for ellipse", () => {
const element = API.createElement({ const element = API.createElement({
type: "ellipse", type: "ellipse",
@ -243,5 +260,57 @@ describe("Test measureText", () => {
}); });
expect(computeContainerHeightForBoundText(element, 150)).toEqual(212); expect(computeContainerHeightForBoundText(element, 150)).toEqual(212);
}); });
it("should compute container height correctly for diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(computeContainerHeightForBoundText(element, 150)).toEqual(300);
});
});
describe("Test getMaxContainerWidth", () => {
const params = {
width: 178,
height: 194,
};
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerWidth(container)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerWidth(container)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerWidth(container)).toBe(79);
});
});
describe("Test getMaxContainerHeight", () => {
const params = {
width: 178,
height: 194,
};
it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerHeight(container)).toBe(184);
});
it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerHeight(container)).toBe(127);
});
it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerHeight(container)).toBe(87);
});
}); });
}); });

View File

@ -12,7 +12,6 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles"; import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { isTextElement } from "."; import { isTextElement } from ".";
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
import { import {
isBoundToContainer, isBoundToContainer,
isImageElement, isImageElement,
@ -244,31 +243,25 @@ const computeBoundTextPosition = (
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container); const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container); const maxContainerWidth = getMaxContainerWidth(container);
const padding = container.type === "ellipse" ? 0 : BOUND_TEXT_PADDING;
let x; let x;
let y; let y;
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) { if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
y = containerCoords.y + padding; y = containerCoords.y;
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) { } else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
y = y = containerCoords.y + (maxContainerHeight - boundTextElement.height);
containerCoords.y +
(maxContainerHeight - boundTextElement.height + padding);
} else { } else {
y = y =
containerCoords.y + containerCoords.y +
(maxContainerHeight / 2 - boundTextElement.height / 2 + padding); (maxContainerHeight / 2 - boundTextElement.height / 2);
} }
if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) { if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) {
x = containerCoords.x + padding; x = containerCoords.x;
} else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) { } else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) {
x = x = containerCoords.x + (maxContainerWidth - boundTextElement.width);
containerCoords.x +
(maxContainerWidth - boundTextElement.width + padding);
} else { } else {
x = x =
containerCoords.x + containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2);
(maxContainerWidth / 2 - boundTextElement.width / 2 + padding);
} }
return { x, y }; return { x, y };
}; };
@ -636,21 +629,23 @@ export const getContainerCenter = (
}; };
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
let offsetX = BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING;
if (container.type === "ellipse") { if (container.type === "ellipse") {
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172 // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
const offsetX = offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2);
(container.width / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2);
const offsetY = }
(container.height / 2) * (1 - Math.sqrt(2) / 2) + BOUND_TEXT_PADDING; // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
if (container.type === "diamond") {
offsetX += container.width / 4;
offsetY += container.height / 4;
}
return { return {
x: container.x + offsetX, x: container.x + offsetX,
y: container.y + offsetY, y: container.y + offsetY,
}; };
}
return {
x: container.x,
y: container.y,
};
}; };
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => { export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
@ -767,5 +762,63 @@ export const computeContainerHeightForBoundText = (
if (isArrowElement(container)) { if (isArrowElement(container)) {
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2; return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
} }
if (container.type === "diamond") {
return 2 * boundTextElementHeight;
}
return boundTextElementHeight + BOUND_TEXT_PADDING * 2; return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
}; };
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
if (containerWidth <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.width;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return containerWidth;
}
if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
if (container.type === "diamond") {
// The width of the largest rectangle inscribed inside a rhombus is
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
}
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return height;
}
if (container.type === "ellipse") {
// The height of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
if (container.type === "diamond") {
// The height of the largest rectangle inscribed inside a rhombus is
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
}
return height - BOUND_TEXT_PADDING * 2;
};

View File

@ -791,7 +791,7 @@ describe("textWysiwyg", () => {
text = h.elements[1] as ExcalidrawTextElementWithContainer; text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.text).toBe("Hello \nWorld!"); expect(text.text).toBe("Hello \nWorld!");
expect(text.originalText).toBe("Hello World!"); expect(text.originalText).toBe("Hello World!");
expect(text.y).toBe(27.5); expect(text.y).toBe(57.5);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2); expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
@ -825,7 +825,7 @@ describe("textWysiwyg", () => {
expect(text.text).toBe("Hello"); expect(text.text).toBe("Hello");
expect(text.originalText).toBe("Hello"); expect(text.originalText).toBe("Hello");
expect(text.y).toBe(40); expect(text.y).toBe(57.5);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING); expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
@ -930,6 +930,8 @@ describe("textWysiwyg", () => {
editor.select(); editor.select();
fireEvent.click(screen.getByTitle("Left")); fireEvent.click(screen.getByTitle("Left"));
await new Promise((r) => setTimeout(r, 0));
fireEvent.click(screen.getByTitle("Align bottom")); fireEvent.click(screen.getByTitle("Align bottom"));
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@ -1278,7 +1280,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [ Array [
15, 15,
20, 25,
] ]
`); `);
}); });
@ -1290,7 +1292,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [ Array [
-25, -25,
20, 25,
] ]
`); `);
}); });
@ -1302,7 +1304,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [ Array [
174, 174,
20, 25,
] ]
`); `);
}); });

View File

@ -24,14 +24,16 @@ import { mutateElement } from "./mutateElement";
import { import {
getApproxLineHeight, getApproxLineHeight,
getBoundTextElementId, getBoundTextElementId,
getBoundTextElementOffset,
getContainerCoords, getContainerCoords,
getContainerDims, getContainerDims,
getContainerElement, getContainerElement,
getTextElementAngle, getTextElementAngle,
getTextWidth, getTextWidth,
normalizeText, normalizeText,
redrawTextBoundingBox,
wrapText, wrapText,
getMaxContainerHeight,
getMaxContainerWidth,
} from "./textElement"; } from "./textElement";
import { import {
actionDecreaseFontSize, actionDecreaseFontSize,
@ -39,7 +41,6 @@ import {
} from "../actions/actionProperties"; } from "../actions/actionProperties";
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas"; import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App"; import App from "../components/App";
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard"; import { parseClipboard } from "../clipboard";
@ -231,10 +232,6 @@ export const textWysiwyg = ({
// Start pushing text upward until a diff of 30px (padding) // Start pushing text upward until a diff of 30px (padding)
// is reached // is reached
else { else {
const padding =
container.type === "ellipse"
? 0
: getBoundTextElementOffset(updatedTextElement);
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
// vertically center align the text // vertically center align the text
@ -245,8 +242,7 @@ export const textWysiwyg = ({
} }
} }
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) { if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY = coordY = containerCoords.y + (maxHeight - textElementHeight);
containerCoords.y + (maxHeight - textElementHeight + padding);
} }
} }
} }
@ -616,6 +612,7 @@ export const textWysiwyg = ({
), ),
}); });
} }
redrawTextBoundingBox(updateElement, container);
} }
onSubmit({ onSubmit({

View File

@ -17,8 +17,11 @@ import { KEYS } from "../keys";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { queryByTestId, queryByText } from "@testing-library/react"; import { queryByTestId, queryByText } from "@testing-library/react";
import { resize, rotate } from "./utils"; import { resize, rotate } from "./utils";
import { getBoundTextElementPosition, wrapText } from "../element/textElement"; import {
import { getMaxContainerWidth } from "../element/newElement"; getBoundTextElementPosition,
wrapText,
getMaxContainerWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement"; import * as textElementUtils from "../element/textElement";
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";