move measurements related utils to textMeasurements.ts
This commit is contained in:
parent
54bf3d9092
commit
e900cb0b64
@ -4,9 +4,9 @@ import { mutateElement } from "../element/mutateElement";
|
|||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import { measureText } from "../element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
|
@ -260,17 +260,19 @@ import throttle from "lodash.throttle";
|
|||||||
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
import { fileOpen, FileSystemHandle } from "../data/filesystem";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getLineHeight,
|
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
isMeasureTextSupported,
|
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
import {
|
||||||
|
getLineHeight,
|
||||||
|
getApproxMinLineHeight,
|
||||||
|
getApproxMinLineWidth,
|
||||||
|
isMeasureTextSupported,
|
||||||
|
} from "../element/textMeasurements";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
normalizeLink,
|
normalizeLink,
|
||||||
|
@ -25,14 +25,13 @@ import {
|
|||||||
getBoundTextElementOffset,
|
getBoundTextElementOffset,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
import { measureText, wrapText } from "./textMeasurements";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
@ -39,15 +39,16 @@ import {
|
|||||||
import { Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineHeight,
|
|
||||||
getApproxMinLineWidth,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
import {
|
||||||
|
getApproxMinLineHeight,
|
||||||
|
getApproxMinLineWidth,
|
||||||
|
} from "./textMeasurements";
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
return angle - 2 * Math.PI;
|
return angle - 2 * Math.PI;
|
||||||
|
@ -1,190 +1,11 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
wrapText,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { ExcalidrawTextElementWithContainer, FontString } from "./types";
|
import { ExcalidrawTextElementWithContainer } from "./types";
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
|
||||||
|
|
||||||
it("shouldn't add new lines for trailing spaces", () => {
|
|
||||||
const text = "Hello whats up ";
|
|
||||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with emojis", () => {
|
|
||||||
const text = "😀";
|
|
||||||
const maxWidth = 1;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe("😀");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the text correctly when max width reached", () => {
|
|
||||||
const text = "Hello😀";
|
|
||||||
const maxWidth = 10;
|
|
||||||
const res = wrapText(text, font, maxWidth);
|
|
||||||
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
|
||||||
const text = "Hello whats up";
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 80,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 140,
|
|
||||||
res: `Hello whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: "Hello whats up",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should push the word if its equal to max width",
|
|
||||||
width: 60,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
|
||||||
const text = `Hello
|
|
||||||
whats up`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 80,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 150,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should respect new lines and ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("When text is long", () => {
|
|
||||||
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width",
|
|
||||||
width: 170,
|
|
||||||
res: `hellolongtextth
|
|
||||||
isiswhatsupwith
|
|
||||||
youIamtypingggg
|
|
||||||
gandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width and break words as per the width",
|
|
||||||
|
|
||||||
width: 130,
|
|
||||||
res: `hellolongte
|
|
||||||
xtthisiswha
|
|
||||||
tsupwithyou
|
|
||||||
Iamtypinggg
|
|
||||||
ggandtyping
|
|
||||||
gg break it
|
|
||||||
now`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
|
||||||
|
|
||||||
width: 600,
|
|
||||||
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
|
||||||
const text = "Hello Excalidraw";
|
|
||||||
// Length of "Excalidraw" is 100 and exacty equal to max width
|
|
||||||
const res = wrapText(text, font, 100);
|
|
||||||
expect(res).toEqual(`Hello
|
|
||||||
Excalidraw`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test measureText", () => {
|
||||||
describe("Test getContainerCoords", () => {
|
describe("Test getContainerCoords", () => {
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontString,
|
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
DEFAULT_FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_SIZE,
|
|
||||||
TEXT_ALIGN,
|
|
||||||
VERTICAL_ALIGN,
|
|
||||||
} from "../constants";
|
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
@ -30,6 +23,7 @@ import {
|
|||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
import { ExtractSetType } from "../utility-types";
|
import { ExtractSetType } from "../utility-types";
|
||||||
|
import { measureText, wrapText } from "./textMeasurements";
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@ -261,270 +255,6 @@ export const computeBoundTextPosition = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
|
||||||
|
|
||||||
export const measureText = (text: string, font: FontString) => {
|
|
||||||
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 height = getTextHeight(text, font);
|
|
||||||
const width = getTextWidth(text, font);
|
|
||||||
|
|
||||||
return { width, height };
|
|
||||||
};
|
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|
||||||
const cacheLineHeight: { [key: FontString]: number } = {};
|
|
||||||
|
|
||||||
export const getLineHeight = (font: FontString) => {
|
|
||||||
if (cacheLineHeight[font]) {
|
|
||||||
return cacheLineHeight[font];
|
|
||||||
}
|
|
||||||
const fontSize = parseInt(font);
|
|
||||||
|
|
||||||
// Calculate line height relative to font size
|
|
||||||
cacheLineHeight[font] = fontSize * 1.2;
|
|
||||||
return cacheLineHeight[font];
|
|
||||||
};
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
|
||||||
|
|
||||||
// since in test env the canvas measureText algo
|
|
||||||
// doesn't measure text and instead just returns number of
|
|
||||||
// characters hence we assume that each letter is 10px
|
|
||||||
const DUMMY_CHAR_WIDTH = 10;
|
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
|
||||||
if (!canvas) {
|
|
||||||
canvas = document.createElement("canvas");
|
|
||||||
}
|
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
|
||||||
canvas2dContext.font = font;
|
|
||||||
const width = canvas2dContext.measureText(text).width;
|
|
||||||
|
|
||||||
/* istanbul ignore else */
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return width * DUMMY_CHAR_WIDTH;
|
|
||||||
}
|
|
||||||
/* istanbul ignore next */
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
||||||
let width = 0;
|
|
||||||
lines.forEach((line) => {
|
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
|
||||||
});
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextHeight = (text: string, font: FontString) => {
|
|
||||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
||||||
const lineHeight = getLineHeight(font);
|
|
||||||
return lineHeight * lines.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|
||||||
const lines: Array<string> = [];
|
|
||||||
const originalLines = text.split("\n");
|
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
|
||||||
|
|
||||||
let currentLine = "";
|
|
||||||
let currentLineWidthTillNow = 0;
|
|
||||||
|
|
||||||
const push = (str: string) => {
|
|
||||||
if (str.trim()) {
|
|
||||||
lines.push(str);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetParams = () => {
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
originalLines.forEach((originalLine) => {
|
|
||||||
const currentLineWidth = getTextWidth(originalLine, font);
|
|
||||||
|
|
||||||
//Push the line if its <= maxWidth
|
|
||||||
if (currentLineWidth <= maxWidth) {
|
|
||||||
lines.push(originalLine);
|
|
||||||
return; // continue
|
|
||||||
}
|
|
||||||
const words = originalLine.split(" ");
|
|
||||||
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
while (index < words.length) {
|
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
|
||||||
|
|
||||||
// This will only happen when single word takes entire width
|
|
||||||
if (currentWordWidth === maxWidth) {
|
|
||||||
push(words[index]);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
|
||||||
else if (currentWordWidth > maxWidth) {
|
|
||||||
// push current line since the current word exceeds the max width
|
|
||||||
// so will be appended in next line
|
|
||||||
push(currentLine);
|
|
||||||
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
while (words[index].length > 0) {
|
|
||||||
const currentChar = String.fromCodePoint(
|
|
||||||
words[index].codePointAt(0)!,
|
|
||||||
);
|
|
||||||
const width = charWidth.calculate(currentChar, font);
|
|
||||||
currentLineWidthTillNow += width;
|
|
||||||
words[index] = words[index].slice(currentChar.length);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
currentLine = currentChar;
|
|
||||||
currentLineWidthTillNow = width;
|
|
||||||
} else {
|
|
||||||
currentLine += currentChar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// push current line if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
resetParams();
|
|
||||||
} else {
|
|
||||||
// space needs to be appended before next word
|
|
||||||
// as currentLine contains chars which couldn't be appended
|
|
||||||
// to previous line
|
|
||||||
currentLine += " ";
|
|
||||||
currentLineWidthTillNow += spaceWidth;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
// Start appending words in a line till max width reached
|
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
|
||||||
const word = words[index];
|
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
|
||||||
push(currentLine);
|
|
||||||
resetParams();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
currentLine += `${word} `;
|
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
const word = currentLine.slice(0, -1);
|
|
||||||
push(word);
|
|
||||||
resetParams();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLine.slice(-1) === " ") {
|
|
||||||
// only remove last trailing space which we have added when joining words
|
|
||||||
currentLine = currentLine.slice(0, -1);
|
|
||||||
push(currentLine);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return lines.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const charWidth = (() => {
|
|
||||||
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
|
||||||
|
|
||||||
const calculate = (char: string, font: FontString) => {
|
|
||||||
const ascii = char.charCodeAt(0);
|
|
||||||
if (!cachedCharWidth[font]) {
|
|
||||||
cachedCharWidth[font] = [];
|
|
||||||
}
|
|
||||||
if (!cachedCharWidth[font][ascii]) {
|
|
||||||
const width = getLineWidth(char, font);
|
|
||||||
cachedCharWidth[font][ascii] = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedCharWidth[font][ascii];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCache = (font: FontString) => {
|
|
||||||
return cachedCharWidth[font];
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
calculate,
|
|
||||||
getCache,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
|
||||||
if (maxCharWidth === 0) {
|
|
||||||
return (
|
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
|
||||||
BOUND_TEXT_PADDING * 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getApproxMinLineHeight = (font: FontString) => {
|
|
||||||
return getLineHeight(font) + 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 getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|
||||||
// Generally lower case is used so converting to lower case
|
|
||||||
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
|
||||||
const batchLength = 6;
|
|
||||||
let index = 0;
|
|
||||||
let widthTillNow = 0;
|
|
||||||
let str = "";
|
|
||||||
while (widthTillNow <= width) {
|
|
||||||
const batch = dummyText.substr(index, index + batchLength);
|
|
||||||
str += batch;
|
|
||||||
widthTillNow += getLineWidth(str, font);
|
|
||||||
if (index === dummyText.length - 1) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
index = index + batchLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (widthTillNow > width) {
|
|
||||||
str = str.substr(0, str.length - 1);
|
|
||||||
widthTillNow = getLineWidth(str, font);
|
|
||||||
}
|
|
||||||
return str.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.length
|
return container?.boundElements?.length
|
||||||
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
||||||
@ -795,14 +525,3 @@ 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;
|
|
||||||
};
|
|
||||||
|
180
src/element/textMeasurements.test.ts
Normal file
180
src/element/textMeasurements.test.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { BOUND_TEXT_PADDING } from "../constants";
|
||||||
|
import { wrapText } from "./textMeasurements";
|
||||||
|
import { FontString } from "./types";
|
||||||
|
|
||||||
|
describe("Test wrapText", () => {
|
||||||
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||||
|
|
||||||
|
it("shouldn't add new lines for trailing spaces", () => {
|
||||||
|
const text = "Hello whats up ";
|
||||||
|
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with emojis", () => {
|
||||||
|
const text = "😀";
|
||||||
|
const maxWidth = 1;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("😀");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the text correctly when max width reached", () => {
|
||||||
|
const text = "Hello😀";
|
||||||
|
const maxWidth = 10;
|
||||||
|
const res = wrapText(text, font, maxWidth);
|
||||||
|
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text doesn't contain new lines", () => {
|
||||||
|
const text = "Hello whats up";
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "break all words when width of each word is less than container width",
|
||||||
|
width: 80,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break all characters when width of each character is less than container width",
|
||||||
|
width: 25,
|
||||||
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break words as per the width",
|
||||||
|
|
||||||
|
width: 140,
|
||||||
|
res: `Hello whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the container",
|
||||||
|
|
||||||
|
width: 250,
|
||||||
|
res: "Hello whats up",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should push the word if its equal to max width",
|
||||||
|
width: 60,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text contain new lines", () => {
|
||||||
|
const text = `Hello
|
||||||
|
whats up`;
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "break all words when width of each word is less than container width",
|
||||||
|
width: 80,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break all characters when width of each character is less than container width",
|
||||||
|
width: 25,
|
||||||
|
res: `H
|
||||||
|
e
|
||||||
|
l
|
||||||
|
l
|
||||||
|
o
|
||||||
|
w
|
||||||
|
h
|
||||||
|
a
|
||||||
|
t
|
||||||
|
s
|
||||||
|
u
|
||||||
|
p`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "break words as per the width",
|
||||||
|
|
||||||
|
width: 150,
|
||||||
|
res: `Hello
|
||||||
|
whats up`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the container",
|
||||||
|
|
||||||
|
width: 250,
|
||||||
|
res: `Hello
|
||||||
|
whats up`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should respect new lines and ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When text is long", () => {
|
||||||
|
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
||||||
|
[
|
||||||
|
{
|
||||||
|
desc: "fit characters of long string as per container width",
|
||||||
|
width: 170,
|
||||||
|
res: `hellolongtextth
|
||||||
|
isiswhatsupwith
|
||||||
|
youIamtypingggg
|
||||||
|
gandtypinggg
|
||||||
|
break it now`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
desc: "fit characters of long string as per container width and break words as per the width",
|
||||||
|
|
||||||
|
width: 130,
|
||||||
|
res: `hellolongte
|
||||||
|
xtthisiswha
|
||||||
|
tsupwithyou
|
||||||
|
Iamtypinggg
|
||||||
|
ggandtyping
|
||||||
|
gg break it
|
||||||
|
now`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
||||||
|
|
||||||
|
width: 600,
|
||||||
|
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
|
||||||
|
break it now`,
|
||||||
|
},
|
||||||
|
].forEach((data) => {
|
||||||
|
it(`should ${data.desc}`, () => {
|
||||||
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
|
expect(res).toEqual(data.res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
||||||
|
const text = "Hello Excalidraw";
|
||||||
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
||||||
|
const res = wrapText(text, font, 100);
|
||||||
|
expect(res).toEqual(`Hello
|
||||||
|
Excalidraw`);
|
||||||
|
});
|
||||||
|
});
|
284
src/element/textMeasurements.ts
Normal file
284
src/element/textMeasurements.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
} from "../constants";
|
||||||
|
import { getFontString, isTestEnv } from "../utils";
|
||||||
|
import { FontString } from "./types";
|
||||||
|
|
||||||
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
|
const cacheLineHeight: { [key: FontString]: number } = {};
|
||||||
|
|
||||||
|
export const getLineHeight = (font: FontString) => {
|
||||||
|
if (cacheLineHeight[font]) {
|
||||||
|
return cacheLineHeight[font];
|
||||||
|
}
|
||||||
|
const fontSize = parseInt(font);
|
||||||
|
|
||||||
|
// Calculate line height relative to font size
|
||||||
|
cacheLineHeight[font] = fontSize * 1.2;
|
||||||
|
return cacheLineHeight[font];
|
||||||
|
};
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
|
// since in test env the canvas measureText algo
|
||||||
|
// doesn't measure text and instead just returns number of
|
||||||
|
// characters hence we assume that each letter is 10px
|
||||||
|
const DUMMY_CHAR_WIDTH = 10;
|
||||||
|
|
||||||
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
|
if (!canvas) {
|
||||||
|
canvas = document.createElement("canvas");
|
||||||
|
}
|
||||||
|
const canvas2dContext = canvas.getContext("2d")!;
|
||||||
|
canvas2dContext.font = font;
|
||||||
|
const width = canvas2dContext.measureText(text).width;
|
||||||
|
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (isTestEnv()) {
|
||||||
|
return width * DUMMY_CHAR_WIDTH;
|
||||||
|
}
|
||||||
|
/* istanbul ignore next */
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
|
let width = 0;
|
||||||
|
lines.forEach((line) => {
|
||||||
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
|
});
|
||||||
|
return width;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTextHeight = (text: string, font: FontString) => {
|
||||||
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
|
const lineHeight = getLineHeight(font);
|
||||||
|
return lineHeight * lines.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureText = (text: string, font: FontString) => {
|
||||||
|
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 height = getTextHeight(text, font);
|
||||||
|
const width = getTextWidth(text, font);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApproxMinLineWidth = (font: FontString) => {
|
||||||
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
|
if (maxCharWidth === 0) {
|
||||||
|
return (
|
||||||
|
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||||
|
BOUND_TEXT_PADDING * 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApproxMinLineHeight = (font: FontString) => {
|
||||||
|
return getLineHeight(font) + BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const charWidth = (() => {
|
||||||
|
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
|
||||||
|
|
||||||
|
const calculate = (char: string, font: FontString) => {
|
||||||
|
const ascii = char.charCodeAt(0);
|
||||||
|
if (!cachedCharWidth[font]) {
|
||||||
|
cachedCharWidth[font] = [];
|
||||||
|
}
|
||||||
|
if (!cachedCharWidth[font][ascii]) {
|
||||||
|
const width = getLineWidth(char, font);
|
||||||
|
cachedCharWidth[font][ascii] = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedCharWidth[font][ascii];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCache = (font: FontString) => {
|
||||||
|
return cachedCharWidth[font];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
calculate,
|
||||||
|
getCache,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** this is not used currently but might be useful
|
||||||
|
* in future hence keeping it
|
||||||
|
*/
|
||||||
|
/* istanbul ignore next */
|
||||||
|
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
||||||
|
// Generally lower case is used so converting to lower case
|
||||||
|
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
||||||
|
const batchLength = 6;
|
||||||
|
let index = 0;
|
||||||
|
let widthTillNow = 0;
|
||||||
|
let str = "";
|
||||||
|
while (widthTillNow <= width) {
|
||||||
|
const batch = dummyText.substr(index, index + batchLength);
|
||||||
|
str += batch;
|
||||||
|
widthTillNow += getLineWidth(str, font);
|
||||||
|
if (index === dummyText.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
index = index + batchLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (widthTillNow > width) {
|
||||||
|
str = str.substr(0, str.length - 1);
|
||||||
|
widthTillNow = getLineWidth(str, font);
|
||||||
|
}
|
||||||
|
return str.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
|
const lines: Array<string> = [];
|
||||||
|
const originalLines = text.split("\n");
|
||||||
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
|
let currentLine = "";
|
||||||
|
let currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
|
const push = (str: string) => {
|
||||||
|
if (str.trim()) {
|
||||||
|
lines.push(str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetParams = () => {
|
||||||
|
currentLine = "";
|
||||||
|
currentLineWidthTillNow = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
originalLines.forEach((originalLine) => {
|
||||||
|
const currentLineWidth = getTextWidth(originalLine, font);
|
||||||
|
|
||||||
|
//Push the line if its <= maxWidth
|
||||||
|
if (currentLineWidth <= maxWidth) {
|
||||||
|
lines.push(originalLine);
|
||||||
|
return; // continue
|
||||||
|
}
|
||||||
|
const words = originalLine.split(" ");
|
||||||
|
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (index < words.length) {
|
||||||
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
|
||||||
|
// This will only happen when single word takes entire width
|
||||||
|
if (currentWordWidth === maxWidth) {
|
||||||
|
push(words[index]);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start breaking longer words exceeding max width
|
||||||
|
else if (currentWordWidth > maxWidth) {
|
||||||
|
// push current line since the current word exceeds the max width
|
||||||
|
// so will be appended in next line
|
||||||
|
push(currentLine);
|
||||||
|
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
while (words[index].length > 0) {
|
||||||
|
const currentChar = String.fromCodePoint(
|
||||||
|
words[index].codePointAt(0)!,
|
||||||
|
);
|
||||||
|
const width = charWidth.calculate(currentChar, font);
|
||||||
|
currentLineWidthTillNow += width;
|
||||||
|
words[index] = words[index].slice(currentChar.length);
|
||||||
|
|
||||||
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
currentLine = currentChar;
|
||||||
|
currentLineWidthTillNow = width;
|
||||||
|
} else {
|
||||||
|
currentLine += currentChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push current line if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
resetParams();
|
||||||
|
} else {
|
||||||
|
// space needs to be appended before next word
|
||||||
|
// as currentLine contains chars which couldn't be appended
|
||||||
|
// to previous line
|
||||||
|
currentLine += " ";
|
||||||
|
currentLineWidthTillNow += spaceWidth;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
// Start appending words in a line till max width reached
|
||||||
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
|
const word = words[index];
|
||||||
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
|
push(currentLine);
|
||||||
|
resetParams();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
currentLine += `${word} `;
|
||||||
|
|
||||||
|
// Push the word if appending space exceeds max width
|
||||||
|
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
||||||
|
const word = currentLine.slice(0, -1);
|
||||||
|
push(word);
|
||||||
|
resetParams();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine.slice(-1) === " ") {
|
||||||
|
// only remove last trailing space which we have added when joining words
|
||||||
|
currentLine = currentLine.slice(0, -1);
|
||||||
|
push(currentLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMeasureTextSupported = () => {
|
||||||
|
const width = getTextWidth(
|
||||||
|
DUMMY_TEXT,
|
||||||
|
getFontString({
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return width > 0;
|
||||||
|
};
|
@ -22,20 +22,15 @@ import {
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getLineHeight,
|
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
|
||||||
measureText,
|
|
||||||
normalizeText,
|
normalizeText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
getTextHeight,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@ -45,6 +40,13 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
import {
|
||||||
|
getLineHeight,
|
||||||
|
getTextWidth,
|
||||||
|
measureText,
|
||||||
|
wrapText,
|
||||||
|
getTextHeight,
|
||||||
|
} from "./textMeasurements";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
|
@ -40,7 +40,6 @@ import {
|
|||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getStroke, StrokeOptions } from "perfect-freehand";
|
import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||||
import {
|
import {
|
||||||
getLineHeight,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
@ -48,6 +47,7 @@ import {
|
|||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { getLineHeight } 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
|
||||||
|
@ -3,7 +3,7 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
|
|||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getLineHeight } from "../element/textElement";
|
import { getLineHeight } from "../element/textMeasurements";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import { NormalizedZoomValue } from "../types";
|
import { NormalizedZoomValue } from "../types";
|
||||||
|
@ -17,7 +17,8 @@ import { KEYS } from "../keys";
|
|||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { queryByTestId, queryByText } from "@testing-library/react";
|
import { queryByTestId, queryByText } from "@testing-library/react";
|
||||||
import { resize, rotate } from "./utils";
|
import { resize, rotate } from "./utils";
|
||||||
import { wrapText, getBoundTextMaxWidth } from "../element/textElement";
|
import { getBoundTextMaxWidth } from "../element/textElement";
|
||||||
|
import { wrapText } from "../element/textMeasurements";
|
||||||
import * as textElementUtils from "../element/textElement";
|
import * as textElementUtils from "../element/textElement";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user