fix: resizing arrow labels (#6789)

* fix arrow labels resizing

- min arrow labels width based on font size
- labels width and padding in % of container's width
- resize labels simply multiplying by scale

* remove no longer needed getContainerDims

* fix arrow labels font size not updated on change font size action

* fix bound arrows not updated right after resize

* fix test

* fix 3+ point arrow label resizing with shift

* fix bound text not scaling when resizing with shift & n or s handle

* fix arrow labels width not updating when moving a 2-point arrow point with shift

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
Alex Kim 2023-08-02 15:04:21 +05:00 committed by GitHub
parent 23c88a38d0
commit bb985eba3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 89 additions and 88 deletions

View File

@ -298,7 +298,6 @@ import {
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement, getBoundTextElement,
getContainerCenter, getContainerCenter,
getContainerDims,
getContainerElement, getContainerElement,
getDefaultLineHeight, getDefaultLineHeight,
getLineHeightInPx, getLineHeightInPx,
@ -3548,9 +3547,8 @@ class App extends React.Component<AppProps, AppState> {
lineHeight, lineHeight,
); );
const minHeight = getApproxMinLineHeight(fontSize, lineHeight); const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const containerDims = getContainerDims(container); const newHeight = Math.max(container.height, minHeight);
const newHeight = Math.max(containerDims.height, minHeight); const newWidth = Math.max(container.width, minWidth);
const newWidth = Math.max(containerDims.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth }); mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2; sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2; sceneY = container.y + newHeight / 2;

View File

@ -117,6 +117,7 @@ export const FRAME_STYLE = {
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji"; export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const MIN_FONT_SIZE = 1;
export const DEFAULT_FONT_SIZE = 20; export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil; export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
export const DEFAULT_TEXT_ALIGN = "left"; export const DEFAULT_TEXT_ALIGN = "left";
@ -239,6 +240,8 @@ export const VERSIONS = {
} as const; } as const;
export const BOUND_TEXT_PADDING = 5; export const BOUND_TEXT_PADDING = 5;
export const ARROW_LABEL_WIDTH_FRACTION = 0.7;
export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11;
export const VERTICAL_ALIGN = { export const VERTICAL_ALIGN = {
TOP: "top", TOP: "top",

View File

@ -264,11 +264,11 @@ export class LinearElementEditor {
}; };
}), }),
); );
}
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
handleBindTextResize(element, false); handleBindTextResize(element, false);
}
} }
// suggest bindings for first and last point if selected // suggest bindings for first and last point if selected

View File

@ -1,4 +1,4 @@
import { SHIFT_LOCKING_ANGLE } from "../constants"; import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points"; import { rescalePoints } from "../points";
import { import {
@ -204,8 +204,6 @@ const rescalePointsInElement = (
} }
: {}; : {};
const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = ( const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>, element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number, nextWidth: number,
@ -589,24 +587,42 @@ export const resizeSingleElement = (
}); });
} }
if (
isArrowElement(element) &&
boundTextElement &&
shouldMaintainAspectRatio
) {
const fontSize =
(resizedElement.width / element.width) * boundTextElement.fontSize;
if (fontSize < MIN_FONT_SIZE) {
return;
}
boundTextFont.fontSize = fontSize;
}
if ( if (
resizedElement.width !== 0 && resizedElement.width !== 0 &&
resizedElement.height !== 0 && resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) && Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y) Number.isFinite(resizedElement.y)
) { ) {
mutateElement(element, resizedElement);
updateBoundElements(element, { updateBoundElements(element, {
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
mutateElement(element, resizedElement);
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize, fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline, baseline: boundTextFont.baseline,
}); });
} }
handleBindTextResize(element, transformHandleDirection); handleBindTextResize(
element,
transformHandleDirection,
shouldMaintainAspectRatio,
);
} }
}; };
@ -722,12 +738,8 @@ export const resizeMultipleElements = (
fontSize?: ExcalidrawTextElement["fontSize"]; fontSize?: ExcalidrawTextElement["fontSize"];
baseline?: ExcalidrawTextElement["baseline"]; baseline?: ExcalidrawTextElement["baseline"];
scale?: ExcalidrawImageElement["scale"]; scale?: ExcalidrawImageElement["scale"];
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
}; };
boundText: {
element: ExcalidrawTextElementWithContainer;
fontSize: ExcalidrawTextElement["fontSize"];
baseline: ExcalidrawTextElement["baseline"];
} | null;
}[] = []; }[] = [];
for (const { orig, latest } of targetElements) { for (const { orig, latest } of targetElements) {
@ -798,50 +810,39 @@ export const resizeMultipleElements = (
} }
} }
let boundText: typeof elementsAndUpdates[0]["boundText"] = null; if (isTextElement(orig)) {
const metrics = measureFontSizeFromWidth(orig, width, height);
const boundTextElement = getBoundTextElement(latest);
if (boundTextElement || isTextElement(orig)) {
const updatedElement = {
...latest,
width,
height,
};
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (orig as ExcalidrawTextElement),
boundTextElement
? getBoundTextMaxWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getBoundTextMaxHeight(updatedElement, boundTextElement)
: updatedElement.height,
);
if (!metrics) { if (!metrics) {
return; return;
} }
update.fontSize = metrics.size;
if (isTextElement(orig)) { update.baseline = metrics.baseline;
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
}
if (boundTextElement) {
boundText = {
element: boundTextElement,
fontSize: metrics.size,
baseline: metrics.baseline,
};
}
} }
elementsAndUpdates.push({ element: latest, update, boundText }); const boundTextElement = pointerDownState.originalElements.get(
getBoundTextElementId(orig) ?? "",
) as ExcalidrawTextElementWithContainer | undefined;
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
if (newFontSize < MIN_FONT_SIZE) {
return;
}
update.boundTextFontSize = newFontSize;
}
elementsAndUpdates.push({
element: latest,
update,
});
} }
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element); const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
for (const { element, update, boundText } of elementsAndUpdates) { for (const {
element,
update: { boundTextFontSize, ...update },
} of elementsAndUpdates) {
const { width, height, angle } = update; const { width, height, angle } = update;
mutateElement(element, update, false); mutateElement(element, update, false);
@ -851,17 +852,17 @@ export const resizeMultipleElements = (
newSize: { width, height }, newSize: { width, height },
}); });
if (boundText) { const boundTextElement = getBoundTextElement(element);
const { element: boundTextElement, ...boundTextUpdates } = boundText; if (boundTextElement && boundTextFontSize) {
mutateElement( mutateElement(
boundTextElement, boundTextElement,
{ {
...boundTextUpdates, fontSize: boundTextFontSize,
angle: isLinearElement(element) ? undefined : angle, angle: isLinearElement(element) ? undefined : angle,
}, },
false, false,
); );
handleBindTextResize(element, transformHandleType); handleBindTextResize(element, transformHandleType, true);
} }
} }

View File

@ -10,6 +10,8 @@ import {
} from "./types"; } from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
ARROW_LABEL_WIDTH_FRACTION,
BOUND_TEXT_PADDING, BOUND_TEXT_PADDING,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
@ -65,7 +67,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
if (container) { if (container) {
maxWidth = getBoundTextMaxWidth(container); maxWidth = getBoundTextMaxWidth(container, textElement);
boundTextUpdates.text = wrapText( boundTextUpdates.text = wrapText(
textElement.originalText, textElement.originalText,
getFontString(textElement), getFontString(textElement),
@ -83,13 +85,12 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline; boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight( const maxContainerHeight = getBoundTextMaxHeight(
container, container,
textElement as ExcalidrawTextElementWithContainer, textElement as ExcalidrawTextElementWithContainer,
); );
let nextHeight = containerDims.height; let nextHeight = container.height;
if (metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText( nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
@ -155,6 +156,7 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = ( export const handleBindTextResize = (
container: NonDeletedExcalidrawElement, container: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => { ) => {
const boundTextElementId = getBoundTextElementId(container); const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId) { if (!boundTextElementId) {
@ -175,15 +177,17 @@ export const handleBindTextResize = (
let text = textElement.text; let text = textElement.text;
let nextHeight = textElement.height; let nextHeight = textElement.height;
let nextWidth = textElement.width; let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getBoundTextMaxWidth(container); const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight( const maxHeight = getBoundTextMaxHeight(
container, container,
textElement as ExcalidrawTextElementWithContainer, textElement as ExcalidrawTextElementWithContainer,
); );
let containerHeight = containerDims.height; let containerHeight = container.height;
let nextBaseLine = textElement.baseline; let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") { if (
shouldMaintainAspectRatio ||
(transformHandleType !== "n" && transformHandleType !== "s")
) {
if (text) { if (text) {
text = wrapText( text = wrapText(
textElement.originalText, textElement.originalText,
@ -207,7 +211,7 @@ export const handleBindTextResize = (
container.type, container.type,
); );
const diff = containerHeight - containerDims.height; const diff = containerHeight - container.height;
// fix the y coord when resizing from ne/nw/n // fix the y coord when resizing from ne/nw/n
const updatedY = const updatedY =
!isArrowElement(container) && !isArrowElement(container) &&
@ -687,16 +691,6 @@ export const getContainerElement = (
return null; return null;
}; };
export const getContainerDims = (element: ExcalidrawElement) => {
const MIN_WIDTH = 300;
if (isArrowElement(element)) {
const width = Math.max(element.width, MIN_WIDTH);
const height = element.height;
return { width, height };
}
return { width: element.width, height: element.height };
};
export const getContainerCenter = ( export const getContainerCenter = (
container: ExcalidrawElement, container: ExcalidrawElement,
appState: AppState, appState: AppState,
@ -887,12 +881,19 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding; return dimension + padding;
}; };
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => { export const getBoundTextMaxWidth = (
const width = getContainerDims(container).width; container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
container,
),
) => {
const { width } = container;
if (isArrowElement(container)) { if (isArrowElement(container)) {
return width - BOUND_TEXT_PADDING * 8 * 2; const minWidth =
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO;
return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth);
} }
if (container.type === "ellipse") { if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is // The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
@ -911,7 +912,7 @@ export const getBoundTextMaxHeight = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
) => { ) => {
const height = getContainerDims(container).height; const { height } = container;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) { if (containerHeight <= 0) {

View File

@ -23,7 +23,6 @@ import { AppState } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
getBoundTextElementId, getBoundTextElementId,
getContainerDims,
getContainerElement, getContainerElement,
getTextElementAngle, getTextElementAngle,
getTextWidth, getTextWidth,
@ -177,20 +176,19 @@ export const textWysiwyg = ({
updatedTextElement, updatedTextElement,
editable, editable,
); );
const containerDims = getContainerDims(container);
let originalContainerData; let originalContainerData;
if (propertiesUpdated) { if (propertiesUpdated) {
originalContainerData = updateOriginalContainerCache( originalContainerData = updateOriginalContainerCache(
container.id, container.id,
containerDims.height, container.height,
); );
} else { } else {
originalContainerData = originalContainerCache[container.id]; originalContainerData = originalContainerCache[container.id];
if (!originalContainerData) { if (!originalContainerData) {
originalContainerData = updateOriginalContainerCache( originalContainerData = updateOriginalContainerCache(
container.id, container.id,
containerDims.height, container.height,
); );
} }
} }
@ -214,7 +212,7 @@ export const textWysiwyg = ({
// autoshrink container height until original container height // autoshrink container height until original container height
// is reached when text is removed // is reached when text is removed
!isArrowElement(container) && !isArrowElement(container) &&
containerDims.height > originalContainerData.height && container.height > originalContainerData.height &&
textElementHeight < maxHeight textElementHeight < maxHeight
) { ) {
const targetContainerHeight = computeContainerDimensionForBoundText( const targetContainerHeight = computeContainerDimensionForBoundText(

View File

@ -1128,7 +1128,7 @@ describe("Test Linear Elements", () => {
height: 500, height: 500,
}); });
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
x: 210, x: -10,
y: 250, y: 250,
width: 400, width: 400,
height: 1, height: 1,
@ -1152,8 +1152,8 @@ describe("Test Linear Elements", () => {
expect( expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard collaboration "Online whiteboard
made easy" collaboration made easy"
`); `);
const handleBindTextResizeSpy = vi.spyOn( const handleBindTextResizeSpy = vi.spyOn(
textElementUtils, textElementUtils,
@ -1165,7 +1165,7 @@ describe("Test Linear Elements", () => {
mouse.moveTo(200, 0); mouse.moveTo(200, 0);
mouse.upAt(200, 0); mouse.upAt(200, 0);
expect(arrow.width).toBe(170); expect(arrow.width).toBe(200);
expect(rect.x).toBe(200); expect(rect.x).toBe(200);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith( expect(handleBindTextResizeSpy).toHaveBeenCalledWith(