Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax

This commit is contained in:
Daniel J. Geiger 2023-04-10 20:24:03 -05:00
commit fb24221587
15 changed files with 287 additions and 115 deletions

View file

@ -184,6 +184,7 @@ export const newTextElement = (
},
);
const offsets = getTextElementPositionOffsets(opts, metrics);
const textElement = newElementWith(
{
..._newElementBase<ExcalidrawTextElement>("text", opts),
@ -196,6 +197,7 @@ export const newTextElement = (
y: opts.y - offsets.y,
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
containerId: opts.containerId || null,
originalText: text,
lineHeight,
@ -213,10 +215,15 @@ const getAdjustedDimensions = (
y: number;
width: number;
height: number;
baseline: number;
} => {
const container = getContainerElement(element);
const { width: nextWidth, height: nextHeight } = measureTextElement(element, {
const {
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
} = measureTextElement(element, {
text: nextText,
});
const { textAlign, verticalAlign } = element;
@ -289,6 +296,7 @@ const getAdjustedDimensions = (
return {
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
x: Number.isFinite(x) ? x : element.x,
y: Number.isFinite(y) ? y : element.y,
};
@ -298,6 +306,9 @@ export const refreshTextDimensions = (
textElement: ExcalidrawTextElement,
text = textElement.text,
) => {
if (textElement.isDeleted) {
return;
}
const container = getContainerElement(textElement);
if (container) {
text = wrapTextElement(textElement, getMaxContainerWidth(container), {

View file

@ -46,6 +46,8 @@ import {
handleBindTextResize,
getMaxContainerWidth,
getApproxMinLineHeight,
measureTextElement,
getMaxContainerHeight,
} from "./textElement";
export const normalizeAngle = (angle: number): number => {
@ -193,7 +195,8 @@ const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number,
): number | null => {
nextHeight: number,
): { size: number; baseline: number } | null => {
// We only use width to scale font on resize
let width = element.width;
@ -208,8 +211,11 @@ const measureFontSizeFromWidth = (
if (nextFontSize < MIN_FONT_SIZE) {
return null;
}
return nextFontSize;
const metrics = measureTextElement(element, { fontSize: nextFontSize });
return {
size: nextFontSize,
baseline: metrics.baseline + (nextHeight - metrics.height),
};
};
const getSidesForTransformHandle = (
@ -280,8 +286,8 @@ const resizeSingleTextElement = (
if (scale > 0) {
const nextWidth = element.width * scale;
const nextHeight = element.height * scale;
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
if (nextFontSize === null) {
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
if (metrics === null) {
return;
}
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
@ -305,9 +311,10 @@ const resizeSingleTextElement = (
deltaY2,
);
mutateElement(element, {
fontSize: nextFontSize,
fontSize: metrics.size,
width: nextWidth,
height: nextHeight,
baseline: metrics.baseline,
x: nextElementX,
y: nextElementY,
});
@ -360,7 +367,7 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFontSize: number | null = null;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) {
@ -410,7 +417,10 @@ export const resizeSingleElement = (
boundTextElement.id,
) as typeof boundTextElement | undefined;
if (stateOfBoundTextElementAtResize) {
boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
boundTextFont = {
fontSize: stateOfBoundTextElementAtResize.fontSize,
baseline: stateOfBoundTextElementAtResize.baseline,
};
}
if (shouldMaintainAspectRatio) {
const updatedElement = {
@ -419,14 +429,18 @@ export const resizeSingleElement = (
height: eleNewHeight,
};
const nextFontSize = measureFontSizeFromWidth(
const nextFont = measureFontSizeFromWidth(
boundTextElement,
getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
);
if (nextFontSize === null) {
if (nextFont === null) {
return;
}
boundTextFontSize = nextFontSize;
boundTextFont = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
};
} else {
const minWidth = getApproxMinLineWidth(
getFontString(boundTextElement),
@ -568,9 +582,10 @@ export const resizeSingleElement = (
});
mutateElement(element, resizedElement);
if (boundTextElement && boundTextFontSize != null) {
if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, {
fontSize: boundTextFontSize,
fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline,
});
}
handleBindTextResize(element, transformHandleDirection);
@ -677,6 +692,7 @@ const resizeMultipleElements = (
y: number;
points?: Point[];
fontSize?: number;
baseline?: number;
} = {
width,
height,
@ -685,7 +701,7 @@ const resizeMultipleElements = (
...rescaledPoints,
};
let boundTextUpdates: { fontSize: number } | null = null;
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest);
@ -695,24 +711,29 @@ const resizeMultipleElements = (
width,
height,
};
const fontSize = measureFontSizeFromWidth(
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement
? getMaxContainerWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getMaxContainerHeight(updatedElement)
: updatedElement.height,
);
if (!fontSize) {
if (!metrics) {
return;
}
if (isTextElement(element.orig)) {
update.fontSize = fontSize;
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
}
if (boundTextElement) {
boundTextUpdates = {
fontSize,
fontSize: metrics.size,
baseline: metrics.baseline,
};
}
}

View file

@ -15,6 +15,7 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
isSafari,
TEXT_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
@ -83,6 +84,7 @@ export const redrawTextBoundingBox = (
text: textElement.text,
width: textElement.width,
height: textElement.height,
baseline: textElement.baseline,
};
boundTextUpdates.text = textElement.text;
@ -97,6 +99,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height;
boundTextUpdates.baseline = metrics.baseline;
// Maintain coordX for non left-aligned text in case the width has changed
if (!container) {
@ -210,13 +213,15 @@ export const handleBindTextResize = (
const maxWidth = getMaxContainerWidth(container);
const maxHeight = getMaxContainerHeight(container);
let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (text) {
text = wrapTextElement(textElement, maxWidth);
}
const dimensions = measureTextElement(textElement, { text });
nextHeight = dimensions.height;
nextWidth = dimensions.width;
const metrics = measureTextElement(textElement, { text });
nextHeight = metrics.height;
nextWidth = metrics.width;
nextBaseLine = metrics.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > maxHeight) {
@ -244,6 +249,7 @@ export const handleBindTextResize = (
text,
width: nextWidth,
height: nextHeight,
baseline: nextBaseLine,
});
if (!isArrowElement(container)) {
@ -304,8 +310,59 @@ export const measureText = (
const fontSize = parseFloat(font);
const height = getTextHeight(text, fontSize, lineHeight);
const width = getTextWidth(text, font);
const baseline = measureBaseline(text, font, lineHeight);
return { width, height, baseline };
};
return { width, height };
export const measureBaseline = (
text: string,
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
wrapInContainer?: boolean,
) => {
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (wrapInContainer) {
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.whiteSpace = "pre-wrap";
}
container.style.lineHeight = String(lineHeight);
container.innerText = text;
// Baseline is important for positioning text on canvas
document.body.appendChild(container);
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.overflow = "hidden";
span.style.width = "1px";
span.style.height = "1px";
container.appendChild(span);
let baseline = span.offsetTop + span.offsetHeight;
const height = container.offsetHeight;
if (isSafari) {
const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight);
const fontSize = parseFloat(font);
// In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs
// from the actual canvas height
const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight);
if (canvasHeight > height) {
baseline += canvasHeight - domHeight;
}
if (height > canvasHeight) {
baseline -= domHeight - canvasHeight;
}
}
document.body.removeChild(container);
return baseline;
};
/**

View file

@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, VERTICAL_ALIGN } from "../constants";
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@ -35,6 +35,7 @@ import {
getMaxContainerHeight,
getMaxContainerWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
} from "./textElement";
import {
actionDecreaseFontSize,
@ -328,13 +329,24 @@ export const textWysiwyg = ({
? offWidth / 2
: 0;
const { width: w, height: h } = updatedTextElement;
let lineHeight = updatedTextElement.lineHeight;
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
if (isSafari) {
lineHeight = detectLineHeight({
...updatedTextElement,
fontSize: Math.round(updatedTextElement.fontSize),
});
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height - viewportY) / appState.zoom.value;
Object.assign(editable.style, {
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: element.lineHeight,
lineHeight,
width: `${Math.min(textElementWidth, maxWidth)}px`,
height: `${textElementHeight}px`,
left: `${viewportX}px`,

View file

@ -10,7 +10,7 @@ import {
import { MarkNonNullable, ValueOf } from "../utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
export type Theme = typeof THEME[keyof typeof THEME];
@ -133,6 +133,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
fontSize: number;
fontFamily: FontFamilyValues;
text: string;
baseline: number;
textAlign: TextAlign;
verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;