Initial implementation of containerBehavior.margin

This commit is contained in:
zsviczian
2025-11-01 11:48:26 +00:00
parent 0ff0efe2a7
commit d668eaa060
12 changed files with 312 additions and 91 deletions

View File

@@ -329,16 +329,24 @@ const generateElementCanvas = (
boundTextCanvasContext.translate(-shiftX, -shiftY);
// Clear the bound text area
boundTextCanvasContext.clearRect(
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
-(
boundTextElement.width / 2 +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
) *
window.devicePixelRatio *
scale,
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) *
-(
boundTextElement.height / 2 +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
) *
window.devicePixelRatio *
scale,
(boundTextElement.width + BOUND_TEXT_PADDING * 2) *
(boundTextElement.width +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
window.devicePixelRatio *
scale,
(boundTextElement.height + BOUND_TEXT_PADDING * 2) *
(boundTextElement.height +
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
window.devicePixelRatio *
scale,
);

View File

@@ -12,6 +12,7 @@ import {
SHIFT_LOCKING_ANGLE,
rescalePoints,
getFontString,
BOUND_TEXT_PADDING,
} from "@excalidraw/common";
import type { GlobalPoint } from "@excalidraw/math";
@@ -741,10 +742,12 @@ export const resizeSingleElement = (
const minWidth = getApproxMinLineWidth(
getFontString(boundTextElement),
boundTextElement.lineHeight,
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
const minHeight = getApproxMinLineHeight(
boundTextElement.fontSize,
boundTextElement.lineHeight,
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
nextWidth = Math.max(nextWidth, minWidth);
nextHeight = Math.max(nextHeight, minHeight);

View File

@@ -108,6 +108,7 @@ export const redrawTextBoundingBox = (
const nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
scene.mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight);
@@ -117,6 +118,7 @@ export const redrawTextBoundingBox = (
const nextWidth = computeContainerDimensionForBoundText(
metrics.width,
container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
scene.mutateElement(container, { width: nextWidth });
}
@@ -187,6 +189,7 @@ export const handleBindTextResize = (
containerHeight = computeContainerDimensionForBoundText(
nextHeight,
container.type,
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
);
const diff = containerHeight - container.height;
@@ -353,8 +356,8 @@ export const getContainerCenter = (
};
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
let offsetX = BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING;
let offsetX = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
let offsetY = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (container.type === "ellipse") {
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
@@ -446,9 +449,10 @@ export const isValidTextContainer = (element: {
export const computeContainerDimensionForBoundText = (
dimension: number,
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
boundTextPadding: number,
) => {
dimension = Math.ceil(dimension);
const padding = BOUND_TEXT_PADDING * 2;
const padding = boundTextPadding * 2;
if (containerType === "ellipse") {
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
@@ -467,6 +471,8 @@ export const getBoundTextMaxWidth = (
boundTextElement: ExcalidrawTextElement | null,
) => {
const { width } = container;
const boundTextPadding =
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (isArrowElement(container)) {
const minWidth =
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
@@ -477,14 +483,14 @@ export const getBoundTextMaxWidth = (
// 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 Math.round((width / 2) * Math.sqrt(2)) - boundTextPadding * 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 Math.round(width / 2) - boundTextPadding * 2;
}
return width - BOUND_TEXT_PADDING * 2;
return width - boundTextPadding * 2;
};
export const getBoundTextMaxHeight = (
@@ -492,8 +498,10 @@ export const getBoundTextMaxHeight = (
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const { height } = container;
const boundTextPadding =
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
const containerHeight = height - boundTextPadding * 8 * 2;
if (containerHeight <= 0) {
return boundTextElement.height;
}
@@ -503,14 +511,14 @@ export const getBoundTextMaxHeight = (
// 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 Math.round((height / 2) * Math.sqrt(2)) - boundTextPadding * 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 Math.round(height / 2) - boundTextPadding * 2;
}
return height - BOUND_TEXT_PADDING * 2;
return height - boundTextPadding * 2;
};
/** retrieves text from text elements and concatenates to a single string */

View File

@@ -32,22 +32,24 @@ const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
export const getApproxMinLineWidth = (
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => {
const maxCharWidth = getMaxCharWidth(font);
if (maxCharWidth === 0) {
return (
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
BOUND_TEXT_PADDING * 2
boundTextPadding * 2
);
}
return maxCharWidth + BOUND_TEXT_PADDING * 2;
return maxCharWidth + boundTextPadding * 2;
};
export const getMinTextElementWidth = (
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => {
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
return measureText("", font, lineHeight).width + boundTextPadding * 2;
};
export const isMeasureTextSupported = () => {
@@ -99,8 +101,9 @@ export const getLineHeightInPx = (
export const getApproxMinLineHeight = (
fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"],
boundTextPadding: number = BOUND_TEXT_PADDING,
) => {
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
return getLineHeightInPx(fontSize, lineHeight) + boundTextPadding * 2;
};
let textMetricsProvider: TextMetricsProvider | undefined;

View File

@@ -1,4 +1,4 @@
import { getLineHeight } from "@excalidraw/common";
import { BOUND_TEXT_PADDING, getLineHeight } from "@excalidraw/common";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
@@ -63,9 +63,13 @@ describe("Test measureText", () => {
type: "rectangle",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
160,
);
expect(
computeContainerDimensionForBoundText(
150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(160);
});
it("should compute container height correctly for ellipse", () => {
@@ -73,9 +77,13 @@ describe("Test measureText", () => {
type: "ellipse",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
226,
);
expect(
computeContainerDimensionForBoundText(
150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(226);
});
it("should compute container height correctly for diamond", () => {
@@ -83,9 +91,13 @@ describe("Test measureText", () => {
type: "diamond",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
320,
);
expect(
computeContainerDimensionForBoundText(
150,
element.type,
BOUND_TEXT_PADDING,
),
).toEqual(320);
});
});