feat: custom text metrics provider (#9121)
This commit is contained in:
parent
c329470b73
commit
e3060dfb8f
@ -10,7 +10,6 @@ import {
|
|||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
@ -35,6 +34,7 @@ import { arrayToMap, getFontString } from "../utils";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { syncMovedIndices } from "../fractionalIndex";
|
import { syncMovedIndices } from "../fractionalIndex";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { measureText } from "../element/textMeasurements";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { measureText } from "../element/textElement";
|
import { measureText } from "../element/textMeasurements";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import type { AppClassProperties } from "../types";
|
import type { AppClassProperties } from "../types";
|
||||||
|
@ -331,17 +331,10 @@ import type { FileSystemHandle } from "../data/filesystem";
|
|||||||
import { fileOpen } from "../data/filesystem";
|
import { fileOpen } from "../data/filesystem";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
|
||||||
getMinTextElementWidth,
|
|
||||||
isMeasureTextSupported,
|
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
measureText,
|
|
||||||
normalizeText,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
showHyperlinkTooltip,
|
showHyperlinkTooltip,
|
||||||
@ -465,6 +458,15 @@ import { cropElement } from "../element/cropElement";
|
|||||||
import { wrapText } from "../element/textWrapping";
|
import { wrapText } from "../element/textWrapping";
|
||||||
import { actionCopyElementLink } from "../actions/actionElementLink";
|
import { actionCopyElementLink } from "../actions/actionElementLink";
|
||||||
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
||||||
|
import {
|
||||||
|
isMeasureTextSupported,
|
||||||
|
normalizeText,
|
||||||
|
measureText,
|
||||||
|
getLineHeightInPx,
|
||||||
|
getApproxMinLineWidth,
|
||||||
|
getApproxMinLineHeight,
|
||||||
|
getMinTextElementWidth,
|
||||||
|
} from "../element/textMeasurements";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
|
@ -7,7 +7,6 @@ import { debounce } from "lodash";
|
|||||||
import type { AppClassProperties } from "../types";
|
import type { AppClassProperties } from "../types";
|
||||||
import { isTextElement, newTextElement } from "../element";
|
import { isTextElement, newTextElement } from "../element";
|
||||||
import type { ExcalidrawTextElement } from "../element/types";
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
import { measureText } from "../element/textElement";
|
|
||||||
import { addEventListener, getFontString } from "../utils";
|
import { addEventListener, getFontString } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@ -20,6 +19,7 @@ import { useStable } from "../hooks/useStable";
|
|||||||
|
|
||||||
import "./SearchMenu.scss";
|
import "./SearchMenu.scss";
|
||||||
import { round } from "../../math";
|
import { round } from "../../math";
|
||||||
|
import { measureText } from "../element/textMeasurements";
|
||||||
|
|
||||||
const searchQueryAtom = atom<string>("");
|
const searchQueryAtom = atom<string>("");
|
||||||
export const searchItemInFocusAtom = atom<number | null>(null);
|
export const searchItemInFocusAtom = atom<number | null>(null);
|
||||||
@ -607,7 +607,6 @@ const getMatchedLines = (
|
|||||||
textToStart,
|
textToStart,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// measureText returns a non-zero width for the empty string
|
// measureText returns a non-zero width for the empty string
|
||||||
@ -621,7 +620,6 @@ const getMatchedLines = (
|
|||||||
lineIndexRange.line,
|
lineIndexRange.line,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const spaceToStart =
|
const spaceToStart =
|
||||||
|
@ -46,7 +46,7 @@ import { bumpVersion } from "../element/mutateElement";
|
|||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { MarkOptional, Mutable } from "../utility-types";
|
import type { MarkOptional, Mutable } from "../utility-types";
|
||||||
import { detectLineHeight, getContainerElement } from "../element/textElement";
|
import { getContainerElement } from "../element/textElement";
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
@ -59,6 +59,7 @@ import {
|
|||||||
} from "../scene";
|
} from "../scene";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { isFiniteNumber, pointFrom } from "../../math";
|
import { isFiniteNumber, pointFrom } from "../../math";
|
||||||
|
import { detectLineHeight } from "../element/textMeasurements";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
newMagicFrameElement,
|
newMagicFrameElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "../element/newElement";
|
} from "../element/newElement";
|
||||||
import { measureText, normalizeText } from "../element/textElement";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
@ -55,6 +54,7 @@ import { syncInvalidIndices } from "../fractionalIndex";
|
|||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
import { isArrowElement } from "../element/typeChecks";
|
import { isArrowElement } from "../element/typeChecks";
|
||||||
import { pointFrom, type LocalPoint } from "../../math";
|
import { pointFrom, type LocalPoint } from "../../math";
|
||||||
|
import { measureText, normalizeText } from "../element/textMeasurements";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
|
@ -10,7 +10,7 @@ import type {
|
|||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
PointerDownState,
|
PointerDownState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getBoundTextElement, getMinTextElementWidth } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
||||||
import { getGridPoint } from "../snapping";
|
import { getGridPoint } from "../snapping";
|
||||||
|
import { getMinTextElementWidth } from "./textMeasurements";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
|
@ -33,11 +33,7 @@ import { getNewGroupIdsForDuplication } from "../groups";
|
|||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import { getBoundTextMaxWidth } from "./textElement";
|
||||||
measureText,
|
|
||||||
normalizeText,
|
|
||||||
getBoundTextMaxWidth,
|
|
||||||
} from "./textElement";
|
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_PROPS,
|
DEFAULT_ELEMENT_PROPS,
|
||||||
@ -51,6 +47,7 @@ import {
|
|||||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
import type { Radians } from "../../math";
|
import type { Radians } from "../../math";
|
||||||
|
import { normalizeText, measureText } from "./textMeasurements";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
@ -41,15 +41,11 @@ import type {
|
|||||||
import type { PointerDownState } from "../types";
|
import type { PointerDownState } from "../types";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getApproxMinLineHeight,
|
|
||||||
measureText,
|
|
||||||
getMinTextElementWidth,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
@ -64,6 +60,12 @@ import {
|
|||||||
type Radians,
|
type Radians,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
|
import {
|
||||||
|
getMinTextElementWidth,
|
||||||
|
measureText,
|
||||||
|
getApproxMinLineWidth,
|
||||||
|
getApproxMinLineHeight,
|
||||||
|
} from "./textMeasurements";
|
||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
||||||
|
@ -6,9 +6,8 @@ import {
|
|||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
detectLineHeight,
|
|
||||||
getLineHeightInPx,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
import { detectLineHeight, getLineHeightInPx } from "./textMeasurements";
|
||||||
import type { ExcalidrawTextElementWithContainer } from "./types";
|
import type { ExcalidrawTextElementWithContainer } from "./types";
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test measureText", () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
import { getFontString, arrayToMap } from "../utils";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -6,7 +6,6 @@ import type {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontString,
|
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
@ -14,7 +13,6 @@ import {
|
|||||||
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
|
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
|
||||||
ARROW_LABEL_WIDTH_FRACTION,
|
ARROW_LABEL_WIDTH_FRACTION,
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
@ -30,18 +28,7 @@ import {
|
|||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import type { ExtractSetType } from "../utility-types";
|
import type { ExtractSetType } from "../utility-types";
|
||||||
|
import { measureText } from "./textMeasurements";
|
||||||
export const normalizeText = (text: string) => {
|
|
||||||
return (
|
|
||||||
normalizeEOL(text)
|
|
||||||
// replace tabs with spaces so they render and measure correctly
|
|
||||||
.replace(/\t/g, " ")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitIntoLines = (text: string) => {
|
|
||||||
return normalizeText(text).split("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
@ -281,201 +268,6 @@ export const computeBoundTextPosition = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const measureText = (
|
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
forceAdvanceWidth?: true,
|
|
||||||
) => {
|
|
||||||
const _text = text
|
|
||||||
.split("\n")
|
|
||||||
// replace empty lines with single space because leading/trailing empty
|
|
||||||
// lines would be stripped from computation
|
|
||||||
.map((x) => x || " ")
|
|
||||||
.join("\n");
|
|
||||||
const fontSize = parseFloat(font);
|
|
||||||
const height = getTextHeight(_text, fontSize, lineHeight);
|
|
||||||
const width = getTextWidth(_text, font, forceAdvanceWidth);
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To get unitless line-height (if unknown) we can calculate it by dividing
|
|
||||||
* height-per-line by fontSize.
|
|
||||||
*/
|
|
||||||
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
|
|
||||||
const lineCount = splitIntoLines(textElement.text).length;
|
|
||||||
return (textElement.height /
|
|
||||||
lineCount /
|
|
||||||
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We calculate the line height from the font size and the unitless line height,
|
|
||||||
* aligning with the W3C spec.
|
|
||||||
*/
|
|
||||||
export const getLineHeightInPx = (
|
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return fontSize * lineHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME rename to getApproxMinContainerHeight
|
|
||||||
export const getApproxMinLineHeight = (
|
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param forceAdvanceWidth use to force retrieve the "advance width" ~ `metrics.width`, instead of the actual boundind box width.
|
|
||||||
*
|
|
||||||
* > The advance width is the distance between the glyph's initial pen position and the next glyph's initial pen position.
|
|
||||||
*
|
|
||||||
* We need to use the advance width as that's the closest thing to the browser wrapping algo, hence using it for:
|
|
||||||
* - text wrapping
|
|
||||||
* - wysiwyg editor (+padding)
|
|
||||||
*
|
|
||||||
* Everything else should be based on the actual bounding box width.
|
|
||||||
*
|
|
||||||
* `Math.ceil` of the final width adds additional buffer which stabilizes slight wrapping incosistencies.
|
|
||||||
*/
|
|
||||||
export const getLineWidth = (
|
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
forceAdvanceWidth?: true,
|
|
||||||
) => {
|
|
||||||
if (!canvas) {
|
|
||||||
canvas = document.createElement("canvas");
|
|
||||||
}
|
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
|
||||||
canvas2dContext.font = font;
|
|
||||||
const metrics = canvas2dContext.measureText(text);
|
|
||||||
|
|
||||||
const advanceWidth = metrics.width;
|
|
||||||
|
|
||||||
// retrieve the actual bounding box width if these metrics are available (as of now > 95% coverage)
|
|
||||||
if (
|
|
||||||
!forceAdvanceWidth &&
|
|
||||||
window.TextMetrics &&
|
|
||||||
"actualBoundingBoxLeft" in window.TextMetrics.prototype &&
|
|
||||||
"actualBoundingBoxRight" in window.TextMetrics.prototype
|
|
||||||
) {
|
|
||||||
// could be negative, therefore getting the absolute value
|
|
||||||
const actualWidth =
|
|
||||||
Math.abs(metrics.actualBoundingBoxLeft) +
|
|
||||||
Math.abs(metrics.actualBoundingBoxRight);
|
|
||||||
|
|
||||||
// fallback to advance width if the actual width is zero, i.e. on text editing start
|
|
||||||
// or when actual width does not respect whitespace chars, i.e. spaces
|
|
||||||
// otherwise actual width should always be bigger
|
|
||||||
return Math.max(actualWidth, advanceWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// since in test env the canvas measureText algo
|
|
||||||
// doesn't measure text and instead just returns number of
|
|
||||||
// characters hence we assume that each letteris 10px
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return advanceWidth * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
return advanceWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextWidth = (
|
|
||||||
text: string,
|
|
||||||
font: FontString,
|
|
||||||
forceAdvanceWidth?: true,
|
|
||||||
) => {
|
|
||||||
const lines = splitIntoLines(text);
|
|
||||||
let width = 0;
|
|
||||||
lines.forEach((line) => {
|
|
||||||
width = Math.max(width, getLineWidth(line, font, forceAdvanceWidth));
|
|
||||||
});
|
|
||||||
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextHeight = (
|
|
||||||
text: string,
|
|
||||||
fontSize: number,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
const lineCount = splitIntoLines(text).length;
|
|
||||||
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const charWidth = (() => {
|
|
||||||
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
|
||||||
|
|
||||||
const calculate = (char: string, font: FontString) => {
|
|
||||||
const unicode = char.charCodeAt(0);
|
|
||||||
if (!cachedCharWidth[font]) {
|
|
||||||
cachedCharWidth[font] = [];
|
|
||||||
}
|
|
||||||
if (!cachedCharWidth[font][unicode]) {
|
|
||||||
const width = getLineWidth(char, font, true);
|
|
||||||
cachedCharWidth[font][unicode] = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedCharWidth[font][unicode];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCache = (font: FontString) => {
|
|
||||||
return cachedCharWidth[font];
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearCache = (font: FontString) => {
|
|
||||||
cachedCharWidth[font] = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
calculate,
|
|
||||||
getCache,
|
|
||||||
clearCache,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|
||||||
|
|
||||||
// FIXME rename to getApproxMinContainerWidth
|
|
||||||
export const getApproxMinLineWidth = (
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
|
||||||
if (maxCharWidth === 0) {
|
|
||||||
return (
|
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
|
||||||
BOUND_TEXT_PADDING * 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinCharWidth = (font: FontString) => {
|
|
||||||
const cache = charWidth.getCache(font);
|
|
||||||
if (!cache) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
|
||||||
|
|
||||||
return Math.min(...cacheWithOutEmpty);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMaxCharWidth = (font: FontString) => {
|
|
||||||
const cache = charWidth.getCache(font);
|
|
||||||
if (!cache) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
|
||||||
return Math.max(...cacheWithOutEmpty);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.length
|
return container?.boundElements?.length
|
||||||
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
|
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
|
||||||
@ -712,24 +504,6 @@ export const getBoundTextMaxHeight = (
|
|||||||
return height - BOUND_TEXT_PADDING * 2;
|
return height - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMeasureTextSupported = () => {
|
|
||||||
const width = getTextWidth(
|
|
||||||
DUMMY_TEXT,
|
|
||||||
getFontString({
|
|
||||||
fontSize: DEFAULT_FONT_SIZE,
|
|
||||||
fontFamily: DEFAULT_FONT_FAMILY,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return width > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinTextElementWidth = (
|
|
||||||
font: FontString,
|
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
|
||||||
) => {
|
|
||||||
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** retrieves text from text elements and concatenates to a single string */
|
/** retrieves text from text elements and concatenates to a single string */
|
||||||
export const getTextFromElements = (
|
export const getTextFromElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
224
packages/excalidraw/element/textMeasurements.ts
Normal file
224
packages/excalidraw/element/textMeasurements.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
} from "../constants";
|
||||||
|
import { getFontString, isTestEnv, normalizeEOL } from "../utils";
|
||||||
|
import type { FontString, ExcalidrawTextElement } from "./types";
|
||||||
|
|
||||||
|
export const measureText = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const _text = text
|
||||||
|
.split("\n")
|
||||||
|
// replace empty lines with single space because leading/trailing empty
|
||||||
|
// lines would be stripped from computation
|
||||||
|
.map((x) => x || " ")
|
||||||
|
.join("\n");
|
||||||
|
const fontSize = parseFloat(font);
|
||||||
|
const height = getTextHeight(_text, fontSize, lineHeight);
|
||||||
|
const width = getTextWidth(_text, font);
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
|
|
||||||
|
// FIXME rename to getApproxMinContainerWidth
|
||||||
|
export const getApproxMinLineWidth = (
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
|
if (maxCharWidth === 0) {
|
||||||
|
return (
|
||||||
|
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||||
|
BOUND_TEXT_PADDING * 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinTextElementWidth = (
|
||||||
|
font: FontString,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMeasureTextSupported = () => {
|
||||||
|
const width = getTextWidth(
|
||||||
|
DUMMY_TEXT,
|
||||||
|
getFontString({
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return width > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeText = (text: string) => {
|
||||||
|
return (
|
||||||
|
normalizeEOL(text)
|
||||||
|
// replace tabs with spaces so they render and measure correctly
|
||||||
|
.replace(/\t/g, " ")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitIntoLines = (text: string) => {
|
||||||
|
return normalizeText(text).split("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get unitless line-height (if unknown) we can calculate it by dividing
|
||||||
|
* height-per-line by fontSize.
|
||||||
|
*/
|
||||||
|
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
|
||||||
|
const lineCount = splitIntoLines(textElement.text).length;
|
||||||
|
return (textElement.height /
|
||||||
|
lineCount /
|
||||||
|
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We calculate the line height from the font size and the unitless line height,
|
||||||
|
* aligning with the W3C spec.
|
||||||
|
*/
|
||||||
|
export const getLineHeightInPx = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return fontSize * lineHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME rename to getApproxMinContainerHeight
|
||||||
|
export const getApproxMinLineHeight = (
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
let textMetricsProvider: TextMetricsProvider | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom text metrics provider.
|
||||||
|
*
|
||||||
|
* Useful for overriding the width calculation algorithm where canvas API is not available / desired.
|
||||||
|
*/
|
||||||
|
export const setCustomTextMetricsProvider = (provider: TextMetricsProvider) => {
|
||||||
|
textMetricsProvider = provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TextMetricsProvider {
|
||||||
|
getLineWidth(text: string, fontString: FontString): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvasTextMetricsProvider implements TextMetricsProvider {
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.canvas = document.createElement("canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to use the advance width as that's the closest thing to the browser wrapping algo, hence using it for:
|
||||||
|
* - text wrapping
|
||||||
|
* - wysiwyg editor (+padding)
|
||||||
|
*
|
||||||
|
* > The advance width is the distance between the glyph's initial pen position and the next glyph's initial pen position.
|
||||||
|
*/
|
||||||
|
public getLineWidth(text: string, fontString: FontString): number {
|
||||||
|
const context = this.canvas.getContext("2d")!;
|
||||||
|
context.font = fontString;
|
||||||
|
const metrics = context.measureText(text);
|
||||||
|
const advanceWidth = metrics.width;
|
||||||
|
|
||||||
|
// since in test env the canvas measureText algo
|
||||||
|
// doesn't measure text and instead just returns number of
|
||||||
|
// characters hence we assume that each letteris 10px
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return advanceWidth * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return advanceWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLineWidth = (text: string, font: FontString) => {
|
||||||
|
if (!textMetricsProvider) {
|
||||||
|
textMetricsProvider = new CanvasTextMetricsProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
return textMetricsProvider.getLineWidth(text, font);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
|
const lines = splitIntoLines(text);
|
||||||
|
let width = 0;
|
||||||
|
lines.forEach((line) => {
|
||||||
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
|
});
|
||||||
|
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextHeight = (
|
||||||
|
text: string,
|
||||||
|
fontSize: number,
|
||||||
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
) => {
|
||||||
|
const lineCount = splitIntoLines(text).length;
|
||||||
|
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const charWidth = (() => {
|
||||||
|
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
||||||
|
|
||||||
|
const calculate = (char: string, font: FontString) => {
|
||||||
|
const unicode = char.charCodeAt(0);
|
||||||
|
if (!cachedCharWidth[font]) {
|
||||||
|
cachedCharWidth[font] = [];
|
||||||
|
}
|
||||||
|
if (!cachedCharWidth[font][unicode]) {
|
||||||
|
const width = getLineWidth(char, font);
|
||||||
|
cachedCharWidth[font][unicode] = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedCharWidth[font][unicode];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCache = (font: FontString) => {
|
||||||
|
return cachedCharWidth[font];
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCache = (font: FontString) => {
|
||||||
|
cachedCharWidth[font] = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
calculate,
|
||||||
|
getCache,
|
||||||
|
clearCache,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const getMinCharWidth = (font: FontString) => {
|
||||||
|
const cache = charWidth.getCache(font);
|
||||||
|
if (!cache) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
||||||
|
|
||||||
|
return Math.min(...cacheWithOutEmpty);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxCharWidth = (font: FontString) => {
|
||||||
|
const cache = charWidth.getCache(font);
|
||||||
|
if (!cache) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
||||||
|
return Math.max(...cacheWithOutEmpty);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { ENV } from "../constants";
|
import { ENV } from "../constants";
|
||||||
import { charWidth, getLineWidth } from "./textElement";
|
import { charWidth, getLineWidth } from "./textMeasurements";
|
||||||
import type { FontString } from "./types";
|
import type { FontString } from "./types";
|
||||||
|
|
||||||
let cachedCjkRegex: RegExp | undefined;
|
let cachedCjkRegex: RegExp | undefined;
|
||||||
@ -385,7 +385,7 @@ export const wrapText = (
|
|||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
|
|
||||||
for (const originalLine of originalLines) {
|
for (const originalLine of originalLines) {
|
||||||
const currentLineWidth = getLineWidth(originalLine, font, true);
|
const currentLineWidth = getLineWidth(originalLine, font);
|
||||||
|
|
||||||
if (currentLineWidth <= maxWidth) {
|
if (currentLineWidth <= maxWidth) {
|
||||||
lines.push(originalLine);
|
lines.push(originalLine);
|
||||||
@ -423,7 +423,7 @@ const wrapLine = (
|
|||||||
// cache single codepoint whitespace, CJK or emoji width calc. as kerning should not apply here
|
// cache single codepoint whitespace, CJK or emoji width calc. as kerning should not apply here
|
||||||
const testLineWidth = isSingleCharacter(token)
|
const testLineWidth = isSingleCharacter(token)
|
||||||
? currentLineWidth + charWidth.calculate(token, font)
|
? currentLineWidth + charWidth.calculate(token, font)
|
||||||
: getLineWidth(testLine, font, true);
|
: getLineWidth(testLine, font);
|
||||||
|
|
||||||
// build up the current line, skipping length check for possibly trailing whitespaces
|
// build up the current line, skipping length check for possibly trailing whitespaces
|
||||||
if (/\s/.test(token) || testLineWidth <= maxWidth) {
|
if (/\s/.test(token) || testLineWidth <= maxWidth) {
|
||||||
@ -443,7 +443,7 @@ const wrapLine = (
|
|||||||
|
|
||||||
// trailing line of the wrapped word might still be joined with next token/s
|
// trailing line of the wrapped word might still be joined with next token/s
|
||||||
currentLine = trailingLine;
|
currentLine = trailingLine;
|
||||||
currentLineWidth = getLineWidth(trailingLine, font, true);
|
currentLineWidth = getLineWidth(trailingLine, font);
|
||||||
iterator = tokenIterator.next();
|
iterator = tokenIterator.next();
|
||||||
} else {
|
} else {
|
||||||
// push & reset, but don't iterate on the next token, as we didn't use it yet!
|
// push & reset, but don't iterate on the next token, as we didn't use it yet!
|
||||||
@ -514,7 +514,7 @@ const wrapWord = (
|
|||||||
* Similarly to browsers, does not trim all trailing whitespaces, but only those exceeding the `maxWidth`.
|
* Similarly to browsers, does not trim all trailing whitespaces, but only those exceeding the `maxWidth`.
|
||||||
*/
|
*/
|
||||||
const trimLine = (line: string, font: FontString, maxWidth: number) => {
|
const trimLine = (line: string, font: FontString, maxWidth: number) => {
|
||||||
const shouldTrimWhitespaces = getLineWidth(line, font, true) > maxWidth;
|
const shouldTrimWhitespaces = getLineWidth(line, font) > maxWidth;
|
||||||
|
|
||||||
if (!shouldTrimWhitespaces) {
|
if (!shouldTrimWhitespaces) {
|
||||||
return line;
|
return line;
|
||||||
@ -527,7 +527,7 @@ const trimLine = (line: string, font: FontString, maxWidth: number) => {
|
|||||||
"",
|
"",
|
||||||
];
|
];
|
||||||
|
|
||||||
let trimmedLineWidth = getLineWidth(trimmedLine, font, true);
|
let trimmedLineWidth = getLineWidth(trimmedLine, font);
|
||||||
|
|
||||||
for (const whitespace of Array.from(whitespaces)) {
|
for (const whitespace of Array.from(whitespaces)) {
|
||||||
const _charWidth = charWidth.calculate(whitespace, font);
|
const _charWidth = charWidth.calculate(whitespace, font);
|
||||||
|
@ -24,8 +24,6 @@ import {
|
|||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
|
||||||
normalizeText,
|
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
@ -50,6 +48,8 @@ import {
|
|||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
|
import { getTextWidth } from "./textMeasurements";
|
||||||
|
import { normalizeText } from "./textMeasurements";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -350,7 +350,7 @@ export const textWysiwyg = ({
|
|||||||
font,
|
font,
|
||||||
getBoundTextMaxWidth(container, boundTextElement),
|
getBoundTextMaxWidth(container, boundTextElement),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font, true);
|
const width = getTextWidth(wrappedText, font);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
getFontFamilyFallbacks,
|
getFontFamilyFallbacks,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { charWidth, getContainerElement } from "../element/textElement";
|
import { getContainerElement } from "../element/textElement";
|
||||||
import { containsCJK } from "../element/textWrapping";
|
import { containsCJK } from "../element/textWrapping";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { getFontString, PromisePool, promiseTry } from "../utils";
|
import { getFontString, PromisePool, promiseTry } from "../utils";
|
||||||
@ -31,6 +31,7 @@ import type {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import type { ValueOf } from "../utility-types";
|
import type { ValueOf } from "../utility-types";
|
||||||
|
import { charWidth } from "../element/textMeasurements";
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
|
@ -295,3 +295,5 @@ export {
|
|||||||
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
||||||
export { getDataURL } from "./data/blob";
|
export { getDataURL } from "./data/blob";
|
||||||
export { isElementLink } from "./element/elementLink";
|
export { isElementLink } from "./element/elementLink";
|
||||||
|
|
||||||
|
export { setCustomTextMetricsProvider } from "./element/textMeasurements";
|
||||||
|
@ -52,7 +52,6 @@ import {
|
|||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
@ -64,6 +63,7 @@ import { getVerticalOffset } from "../fonts";
|
|||||||
import { isRightAngleRads } from "../../math";
|
import { isRightAngleRads } from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
import { getUncroppedImageElement } from "../element/cropElement";
|
import { getUncroppedImageElement } from "../element/cropElement";
|
||||||
|
import { getLineHeightInPx } from "../element/textMeasurements";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
|
@ -16,7 +16,6 @@ import { LinearElementEditor } from "../element/linearElementEditor";
|
|||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
@ -38,6 +37,7 @@ import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
|||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
import { getUncroppedWidthAndHeight } from "../element/cropElement";
|
import { getUncroppedWidthAndHeight } from "../element/cropElement";
|
||||||
|
import { getLineHeightInPx } from "../element/textMeasurements";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
|
@ -5,7 +5,7 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
|
|||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getLineHeightInPx } from "../element/textElement";
|
import { getLineHeightInPx } from "../element/textMeasurements";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import type { NormalizedZoomValue } from "../types";
|
import type { NormalizedZoomValue } from "../types";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user