Compare commits

...

11 Commits

Author SHA1 Message Date
Aakansha Doshi
2abc4c2ac1 don't decrease height beyond min height 2023-03-06 23:33:39 +05:30
Aakansha Doshi
9025ad99fc Add tests 2023-03-06 16:47:08 +05:30
Aakansha Doshi
357a1c47f6 Merge remote-tracking branch 'origin/master' into aakansha-disable-scaling-boundtext 2023-03-06 15:51:40 +05:30
Aakansha Doshi
752c7cf66e add utility to compute container coords from bound text 2023-03-06 15:40:14 +05:30
Aakansha Doshi
83780b91d2 rename getContainerCoords -> computeBoundTextElementCoords 2023-03-06 15:31:38 +05:30
Aakansha Doshi
fefd377408 fix coords when bound text height overflows during shift resize 2023-03-06 15:27:02 +05:30
Aakansha Doshi
d3d7244993 wrap text when resizing vertically with shift 2023-03-06 12:09:11 +05:30
Aakansha Doshi
b3068a5248 fix 2023-03-01 20:21:02 +05:30
Aakansha Doshi
caa22dd4c3 lint 2023-02-28 20:49:48 +05:30
Aakansha Doshi
78e5816459 fix 2023-02-28 20:45:48 +05:30
Aakansha Doshi
0b30a23694 fix: disable scaling bound text when using shift resize 2023-02-28 20:32:19 +05:30
6 changed files with 351 additions and 155 deletions

View File

@ -43,9 +43,9 @@ import {
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getMaxContainerWidth,
getContainerElement,
} from "./textElement";
export const normalizeAngle = (angle: number): number => {
@ -414,29 +414,11 @@ export const resizeSingleElement = (
fontSize: stateOfBoundTextElementAtResize.fontSize,
};
}
if (shouldMaintainAspectRatio) {
const updatedElement = {
...element,
width: eleNewWidth,
height: eleNewHeight,
};
const nextFontSize = measureFontSizeFromWidth(
boundTextElement,
getMaxContainerWidth(updatedElement),
);
if (nextFontSize === null) {
return;
}
boundTextFont = {
fontSize: nextFontSize,
};
} else {
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
}
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
}
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
@ -569,7 +551,11 @@ export const resizeSingleElement = (
if (boundTextElement && boundTextFont) {
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
}
handleBindTextResize(element, transformHandleDirection);
handleBindTextResize(
element,
transformHandleDirection,
shouldMaintainAspectRatio,
);
}
};

View File

@ -2,10 +2,11 @@ import { BOUND_TEXT_PADDING } from "../constants";
import { API } from "../tests/helpers/api";
import {
computeContainerDimensionForBoundText,
getContainerCoords,
computeBoundTextElementCoords,
getMaxContainerWidth,
getMaxContainerHeight,
wrapText,
computeContainerCoords,
} from "./textElement";
import { FontString } from "./types";
@ -177,122 +178,150 @@ break it now`,
});
});
describe("Test measureText", () => {
describe("Test getContainerCoords", () => {
const params = { width: 200, height: 100, x: 10, y: 20 };
describe("Test computeBoundTextElementCoords", () => {
const params = { width: 200, height: 100, x: 10, y: 20 };
it("should compute coords correctly when ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 44.2893218813452455,
y: 39.64466094067262,
});
it("should compute coords correctly when ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
it("should compute coords correctly when rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 15,
y: 25,
});
});
it("should compute coords correctly when diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 65,
y: 50,
});
expect(computeBoundTextElementCoords(element)).toEqual({
x: 44.2893218813452455,
y: 39.64466094067262,
});
});
describe("Test computeContainerDimensionForBoundText", () => {
const params = {
width: 178,
height: 194,
};
it("should compute container height correctly for rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
160,
);
it("should compute coords correctly when rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
it("should compute container height correctly for ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
226,
);
});
it("should compute container height correctly for diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
320,
);
expect(computeBoundTextElementCoords(element)).toEqual({
x: 15,
y: 25,
});
});
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 compute coords correctly when diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
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);
expect(computeBoundTextElementCoords(element)).toEqual({
x: 65,
y: 50,
});
});
});
describe("Test computeContainerCoords", () => {
const boundTextElement = API.createElement({
type: "text",
width: 200,
height: 100,
x: 10,
y: 20,
});
it("should compute coords correctly when ellipse", () => {
expect(computeContainerCoords(boundTextElement, "ellipse")).toEqual({
x: -24.289321881345245,
y: 0.3553390593273775,
});
});
it("should compute coords correctly when rectangle", () => {
expect(computeContainerCoords(boundTextElement, "rectangle")).toEqual({
x: 5,
y: 15,
});
});
it("should compute coords correctly when diamond", () => {
expect(computeContainerCoords(boundTextElement, "diamond")).toEqual({
x: -45,
y: -10,
});
});
});
describe("Test computeContainerDimensionForBoundText", () => {
const params = {
width: 178,
height: 194,
};
it("should compute container height correctly for rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
160,
);
});
it("should compute container height correctly for ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
226,
);
});
it("should compute container height correctly for diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
320,
);
});
});
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

@ -147,6 +147,7 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = (
container: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => {
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId) {
@ -184,7 +185,7 @@ export const handleBindTextResize = (
nextWidth = dimensions.width;
}
// increase height in case text element height exceeds
if (nextHeight > maxHeight) {
if (!shouldMaintainAspectRatio && nextHeight > maxHeight) {
containerHeight = computeContainerDimensionForBoundText(
nextHeight,
container.type,
@ -205,6 +206,53 @@ export const handleBindTextResize = (
});
}
if (
shouldMaintainAspectRatio &&
(nextHeight > maxHeight || nextWidth > maxWidth)
) {
let height = containerDims.height;
let width = containerDims.width;
let x = container.x;
let y = container.y;
if (nextHeight > maxHeight) {
height = computeContainerDimensionForBoundText(
nextHeight,
container.type,
);
}
if (nextWidth > maxWidth) {
width = computeContainerDimensionForBoundText(
nextWidth,
container.type,
);
}
const diffX = width - containerDims.width;
const diffY = height - containerDims.height;
if (transformHandleType === "n") {
y = container.y - diffY;
} else if (
transformHandleType === "e" ||
transformHandleType === "w" ||
transformHandleType === "ne" ||
transformHandleType === "nw"
) {
y = container.y - diffY / 2;
}
if (transformHandleType === "s" || transformHandleType === "n") {
x = container.x - diffX / 2;
}
mutateElement(container, {
height,
width,
x,
y,
});
}
mutateElement(textElement, {
text,
width: nextWidth,
@ -227,7 +275,7 @@ const computeBoundTextPosition = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const containerCoords = getContainerCoords(container);
const containerCoords = computeBoundTextElementCoords(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
@ -596,7 +644,9 @@ export const getContainerCenter = (
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
};
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
export const computeBoundTextElementCoords = (
container: NonDeletedExcalidrawElement,
) => {
let offsetX = BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING;
@ -616,6 +666,27 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
};
};
export const computeContainerCoords = (
boundTextElement: ExcalidrawTextElement,
containerType: string,
) => {
let offsetX = BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING;
if (containerType === "ellipse") {
offsetX += (boundTextElement.width / 2) * (1 - Math.sqrt(2) / 2);
offsetY += (boundTextElement.height / 2) * (1 - Math.sqrt(2) / 2);
}
if (containerType === "diamond") {
offsetX += boundTextElement.width / 4;
offsetY += boundTextElement.height / 4;
}
return {
x: boundTextElement.x - offsetX,
y: boundTextElement.y - offsetY,
};
};
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
const container = getContainerElement(textElement);
if (!container || isArrowElement(container)) {

View File

@ -989,26 +989,136 @@ describe("textWysiwyg", () => {
]);
});
it("should scale font size correctly when resizing using shift", async () => {
Keyboard.keyPress(KEYS.ENTER);
describe.only("when using shift resize", () => {
it("should wrap text correctly when resizing using shift from 'ne' handle", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello" } });
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
expect(rectangle.height).toBe(75);
expect(textElement.fontSize).toBe(20);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, {
target: { value: "Excalidraw is an opensource virtual whiteboard" },
});
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
expect(rectangle.height).toBe(202);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excalid
raw is
an
opensou
rce
virtual
whitebo
ard`,
);
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
shift: true,
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
shift: true,
});
expect(rectangle.width).toBe(200);
expect(rectangle.height).toBe(449);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excalidraw is an
opensource virtual
whiteboard`,
);
});
it("should wrap text correctly when resizing using shift vertically using 'n' handle", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, {
target: { value: "Excalidraw is an opensource virtual whiteboard" },
});
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
expect(rectangle.height).toBe(202);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excalid
raw is
an
opensou
rce
virtual
whitebo
ard`,
);
resize(rectangle, "n", [rectangle.x + 30, rectangle.y - 50], {
shift: true,
});
expect(rectangle.width).toBe(104);
expect(rectangle.height).toBe(232);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excalid
raw is
an
opensou
rce
virtual
whitebo
ard`,
);
});
it("should wrap text correctly when resizing using shift horizontally and text overflows", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, {
target: { value: "Excalidraw is an opensource virtual whiteboard" },
});
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
expect(rectangle.height).toBe(202);
expect(rectangle.y).toBe(20);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excalid
raw is
an
opensou
rce
virtual
whitebo
ard`,
);
resize(rectangle, "e", [rectangle.x - 30, rectangle.y + 30], {
shift: true,
});
expect(rectangle.width).toBe(70);
expect(rectangle.height).toBe(226);
expect(rectangle.y).toBe(8);
expect(textElement.fontSize).toBe(20);
expect(textElement.text).toBe(
`Excal
idraw
is an
opens
ource
virtu
al
white
board`,
);
});
expect(rectangle.width).toBe(200);
expect(rectangle.height).toBe(166.66666666666669);
expect(textElement.fontSize).toBe(47.5);
});
it("should bind text correctly when container duplicated with alt-drag", async () => {

View File

@ -24,7 +24,7 @@ import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
getContainerCoords,
computeBoundTextElementCoords,
getContainerDims,
getContainerElement,
getTextElementAngle,
@ -233,7 +233,7 @@ export const textWysiwyg = ({
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const containerCoords = getContainerCoords(container);
const containerCoords = computeBoundTextElementCoords(container);
// vertically center align the text
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {

View File

@ -42,7 +42,7 @@ import { getStroke, StrokeOptions } from "perfect-freehand";
import {
getApproxLineHeight,
getBoundTextElement,
getContainerCoords,
computeBoundTextElementCoords,
getContainerElement,
getMaxContainerHeight,
getMaxContainerWidth,
@ -820,7 +820,7 @@ const drawElementFromCanvas = (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
hasBoundTextElement(element)
) {
const coords = getContainerCoords(element);
const coords = computeBoundTextElementCoords(element);
context.strokeStyle = "#c92a2a";
context.lineWidth = 3;
context.strokeRect(