mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Support labels for arrow 🔥 (#5723)
* feat: support arrow with text * render arrow -> clear rect-> render text * move bound text when linear elements move * fix centering cursor when linear element rotated * fix y coord when new line added and container has 3 points * update text position when 2nd point moved * support adding label on top of 2nd point when 3 points are present * change linear element editor shortcut to cmd+enter and fix tests * scale bound text points when resizing via bounding box * ohh yeah rotation works :) * fix coords when updating text properties * calculate new position after rotation always from original position * rotate the bound text by same angle as parent * don't rotate text and make sure dimensions and coords are always calculated from original point * hardcoding the text width for now * Move the linear element when bound text hit * Rotation working yaay * consider text element angle when editing * refactor * update x2 coords if needed when text updated * simplify * consider bound text to be part of bounding box when hit * show bounding box correctly when multiple element selected * fix typo * support rotating multiple elements * support multiple element resizing * shift bound text to mid point when odd points * Always render linear element handles inside editor after element rendered so point is visible for bound text * Delete bound text when point attached to it deleted * move bound to mid segement mid point when points are even * shift bound text when points nearby deleted and handle segment deletion * Resize working :) * more resize fixes * don't update cache-its breaking delete points, look for better soln * update mid point cache for bound elements when updated * introduce wrapping when resizing * wrap when resize for 2 pointer linear elements * support adding text for linear elements with more than 3 points * export to svg working :) * clip from nearest enclosing element with non transparent color if present when exporting and fill with correct color in canvas * fix snap * use visible elements * Make export to svg work with Mask :) * remove id * mask canvas linear element area where label is added * decide the position of bound text during render * fix coords when editing * fix multiple resize * update cache when bound text version changes * fix masking when rotated * render text in correct position in preview * remove unnecessary code * fix masking when rotating linear element * fix masking with zoom * fix mask in preview for export * fix offsets in export view * fix coords on svg export * fix mask when element rotated in svg * enable double-click to enter text * fix hint * Position cursor correctly and text dimensiosn when height of element is negative * don't allow 2 pointer linear element with bound text width to go beyond min width * code cleanup * fix freedraw * Add padding * don't show vertical align action for linear element containers * Add specs for getBoundTextElementPosition * more specs * move some utils to linearElementEditor.ts * remove only :p * check absoulte coods in test * Add test to hide vertical align for linear eleemnt with bound text * improve export preview * support labels only for arrows * spec * fix large texts * fix tests * fix zooming * enter line editor with cmd+double click * Allow points to move beyond min width/height for 2 pointer arrow with bound text * fix hint for line editing * attempt to fix arrow getting deselected * fix hint and shortcut * Add padding of 5px when creating bound text and add spec * Wrap bound text when arrow binding containers moved * Add spec * remove * set boundTextElementVersion to null if not present * dont use cache when version mismatch * Add a padding of 5px vertically when creating text * Add box sizing content box * Set bound elements when text element created to fix the padding * fix zooming in editor * fix zoom in export * remove globalCompositeOperation and use clearRect instead of fillRect
This commit is contained in:
parent
1933116261
commit
760fd7b3a6
25 changed files with 1668 additions and 363 deletions
|
@ -26,6 +26,7 @@ import Scene from "../scene/Scene";
|
|||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
|
||||
export type SuggestedBinding =
|
||||
| NonDeleted<ExcalidrawBindableElement>
|
||||
|
@ -361,6 +362,10 @@ export const updateBoundElements = (
|
|||
endBinding,
|
||||
changedElement as ExcalidrawBindableElement,
|
||||
);
|
||||
const boundText = getBoundTextElement(element);
|
||||
if (boundText) {
|
||||
handleBindTextResize(element, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
} from "./types";
|
||||
import { distance2d, rotate } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
|
@ -13,8 +14,15 @@ import {
|
|||
getShapeForElement,
|
||||
generateRoughOptions,
|
||||
} from "../renderer/renderElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
import {
|
||||
isArrowElement,
|
||||
isFreeDrawElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { rescalePoints } from "../points";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
// x and y position of top left corner, x and y position of bottom right corner
|
||||
export type Bounds = readonly [number, number, number, number];
|
||||
|
@ -24,17 +32,39 @@ type MaybeQuadraticSolution = [number | null, number | null] | false;
|
|||
// This set of functions retrieves the absolute position of the 4 points.
|
||||
export const getElementAbsoluteCoords = (
|
||||
element: ExcalidrawElement,
|
||||
): Bounds => {
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
if (isFreeDrawElement(element)) {
|
||||
return getFreeDrawElementAbsoluteCoords(element);
|
||||
} else if (isLinearElement(element)) {
|
||||
return getLinearElementAbsoluteCoords(element);
|
||||
return LinearElementEditor.getElementAbsoluteCoords(
|
||||
element,
|
||||
includeBoundText,
|
||||
);
|
||||
} else if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
if (isArrowElement(container)) {
|
||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
element as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
return [
|
||||
coords.x,
|
||||
coords.y,
|
||||
coords.x + element.width,
|
||||
coords.y + element.height,
|
||||
coords.x + element.width / 2,
|
||||
coords.y + element.height / 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
return [
|
||||
element.x,
|
||||
element.y,
|
||||
element.x + element.width,
|
||||
element.y + element.height,
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -159,7 +189,7 @@ const getCubicBezierCurveBound = (
|
|||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
const getMinMaxXYFromCurvePathOps = (
|
||||
export const getMinMaxXYFromCurvePathOps = (
|
||||
ops: Op[],
|
||||
transformXY?: (x: number, y: number) => [number, number],
|
||||
): [number, number, number, number] => {
|
||||
|
@ -230,59 +260,13 @@ const getBoundsFromPoints = (
|
|||
|
||||
const getFreeDrawElementAbsoluteCoords = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
): [number, number, number, number] => {
|
||||
): [number, number, number, number, number, number] => {
|
||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
||||
|
||||
return [
|
||||
minX + element.x,
|
||||
minY + element.y,
|
||||
maxX + element.x,
|
||||
maxY + element.y,
|
||||
];
|
||||
};
|
||||
|
||||
const getLinearElementAbsoluteCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
): [number, number, number, number] => {
|
||||
let coords: [number, number, number, number];
|
||||
|
||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||
// XXX this is just a poor estimate and not very useful
|
||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||
(limits, [x, y]) => {
|
||||
limits.minY = Math.min(limits.minY, y);
|
||||
limits.minX = Math.min(limits.minX, x);
|
||||
|
||||
limits.maxX = Math.max(limits.maxX, x);
|
||||
limits.maxY = Math.max(limits.maxY, y);
|
||||
|
||||
return limits;
|
||||
},
|
||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||
);
|
||||
coords = [
|
||||
minX + element.x,
|
||||
minY + element.y,
|
||||
maxX + element.x,
|
||||
maxY + element.y,
|
||||
];
|
||||
} else {
|
||||
const shape = getShapeForElement(element)!;
|
||||
|
||||
// first element is always the curve
|
||||
const ops = getCurvePathOps(shape[0]);
|
||||
|
||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||
|
||||
coords = [
|
||||
minX + element.x,
|
||||
minY + element.y,
|
||||
maxX + element.x,
|
||||
maxY + element.y,
|
||||
];
|
||||
}
|
||||
|
||||
return coords;
|
||||
const x1 = minX + element.x;
|
||||
const y1 = minY + element.y;
|
||||
const x2 = maxX + element.x;
|
||||
const y2 = maxY + element.y;
|
||||
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
||||
};
|
||||
|
||||
export const getArrowheadPoints = (
|
||||
|
@ -420,7 +404,23 @@ const getLinearElementRotatedBounds = (
|
|||
cy,
|
||||
element.angle,
|
||||
);
|
||||
return [x, y, x, y];
|
||||
|
||||
let coords: [number, number, number, number] = [x, y, x, y];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
[x, y, x, y],
|
||||
boundTextElement,
|
||||
);
|
||||
coords = [
|
||||
coordsWithBoundText[0],
|
||||
coordsWithBoundText[1],
|
||||
coordsWithBoundText[2],
|
||||
coordsWithBoundText[3],
|
||||
];
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
// first element is always the curve
|
||||
|
@ -429,8 +429,28 @@ const getLinearElementRotatedBounds = (
|
|||
const ops = getCurvePathOps(shape);
|
||||
const transformXY = (x: number, y: number) =>
|
||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||
|
||||
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
let coords: [number, number, number, number] = [
|
||||
res[0],
|
||||
res[1],
|
||||
res[2],
|
||||
res[3],
|
||||
];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
coords,
|
||||
boundTextElement,
|
||||
);
|
||||
coords = [
|
||||
coordsWithBoundText[0],
|
||||
coordsWithBoundText[1],
|
||||
coordsWithBoundText[2],
|
||||
coordsWithBoundText[3],
|
||||
];
|
||||
}
|
||||
return coords;
|
||||
};
|
||||
|
||||
// We could cache this stuff
|
||||
|
@ -439,9 +459,7 @@ export const getElementBounds = (
|
|||
): [number, number, number, number] => {
|
||||
let bounds: [number, number, number, number];
|
||||
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||
if (isFreeDrawElement(element)) {
|
||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||
element.points.map(([x, y]) =>
|
||||
|
|
|
@ -36,6 +36,7 @@ import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
|||
import { isTextElement } from ".";
|
||||
import { isTransparent } from "../utils";
|
||||
import { shouldShowBoundingBox } from "./transformHandles";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
|
||||
const isElementDraggableFromInside = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
|
@ -72,6 +73,13 @@ export const hitTest = (
|
|||
return isPointHittingElementBoundingBox(element, point, threshold);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const isHittingBoundTextElement = hitTest(boundTextElement, appState, x, y);
|
||||
if (isHittingBoundTextElement) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return isHittingElementNotConsideringBoundingBox(element, appState, point);
|
||||
};
|
||||
|
||||
|
@ -83,6 +91,13 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
|
|||
): boolean => {
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
|
||||
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
|
||||
// eg for linear elements text can be outside the element bounding box
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement && hitTest(boundTextElement, appState, x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
|
||||
isPointHittingElementBoundingBox(element, [x, y], threshold)
|
||||
|
@ -95,7 +110,6 @@ export const isHittingElementNotConsideringBoundingBox = (
|
|||
point: Point,
|
||||
): boolean => {
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
|
||||
const check = isTextElement(element)
|
||||
? isStrictlyInside
|
||||
: isElementDraggableFromInside(element)
|
||||
|
@ -382,6 +396,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
|
|||
if (!getShapeForElement(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [point, pointAbs, hwidth, hheight] = pointRelativeToElement(
|
||||
args.element,
|
||||
args.point,
|
||||
|
@ -434,8 +449,9 @@ const pointRelativeToElement = (
|
|||
pointTuple: Point,
|
||||
): [GA.Point, GA.Point, number, number] => {
|
||||
const point = GAPoint.from(pointTuple);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const elementCoords = getElementAbsoluteCoords(element);
|
||||
const center = coordsCenter(elementCoords);
|
||||
const center = coordsCenter([x1, y1, x2, y2]);
|
||||
// GA has angle orientation opposite to `rotate`
|
||||
const rotate = GATransform.rotation(center, element.angle);
|
||||
const pointRotated = GATransform.apply(rotate, point);
|
||||
|
@ -466,8 +482,8 @@ export const pointInAbsoluteCoords = (
|
|||
const relativizationToElementCenter = (
|
||||
element: ExcalidrawElement,
|
||||
): GA.Transform => {
|
||||
const elementCoords = getElementAbsoluteCoords(element);
|
||||
const center = coordsCenter(elementCoords);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const center = coordsCenter([x1, y1, x2, y2]);
|
||||
// GA has angle orientation opposite to `rotate`
|
||||
const rotate = GATransform.rotation(center, element.angle);
|
||||
const translate = GA.reverse(
|
||||
|
@ -524,8 +540,8 @@ export const determineFocusPoint = (
|
|||
adjecentPoint: Point,
|
||||
): Point => {
|
||||
if (focus === 0) {
|
||||
const elementCoords = getElementAbsoluteCoords(element);
|
||||
const center = coordsCenter(elementCoords);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const center = coordsCenter([x1, y1, x2, y2]);
|
||||
return GAPoint.toTuple(center);
|
||||
}
|
||||
const relateToCenter = relativizationToElementCenter(element);
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
ExcalidrawElement,
|
||||
PointBinding,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
} from "./types";
|
||||
import {
|
||||
distance2d,
|
||||
|
@ -19,7 +20,11 @@ import {
|
|||
arePointsEqual,
|
||||
} from "../math";
|
||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||
import { getElementPointsCoords } from "./bounds";
|
||||
import {
|
||||
getCurvePathOps,
|
||||
getElementPointsCoords,
|
||||
getMinMaxXYFromCurvePathOps,
|
||||
} from "./bounds";
|
||||
import { Point, AppState, PointerCoords } from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import History from "../history";
|
||||
|
@ -33,6 +38,8 @@ import {
|
|||
import { tupleToCoors } from "../utils";
|
||||
import { isBindingElement } from "./typeChecks";
|
||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { DRAGGING_THRESHOLD } from "../constants";
|
||||
|
||||
const editorMidPointsCache: {
|
||||
|
@ -40,7 +47,6 @@ const editorMidPointsCache: {
|
|||
points: (Point | null)[];
|
||||
zoom: number | null;
|
||||
} = { version: null, points: [], zoom: null };
|
||||
|
||||
export class LinearElementEditor {
|
||||
public readonly elementId: ExcalidrawElement["id"] & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
|
@ -257,6 +263,11 @@ export class LinearElementEditor {
|
|||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, false);
|
||||
}
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
|
@ -388,8 +399,14 @@ export class LinearElementEditor {
|
|||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
// Since its not needed outside editor unless 2 pointer lines
|
||||
if (!appState.editingLinearElement && element.points.length > 2) {
|
||||
const boundText = getBoundTextElement(element);
|
||||
|
||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||
if (
|
||||
!appState.editingLinearElement &&
|
||||
element.points.length > 2 &&
|
||||
!boundText
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
|
@ -661,7 +678,6 @@ export class LinearElementEditor {
|
|||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
|
||||
// if we clicked on a point, set the element as hitElement otherwise
|
||||
// it would get deselected if the point is outside the hitbox area
|
||||
if (clickedPointIndex >= 0 || segmentMidpoint) {
|
||||
|
@ -1055,7 +1071,6 @@ export class LinearElementEditor {
|
|||
const offsetY = 0;
|
||||
|
||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
}
|
||||
|
||||
|
@ -1223,7 +1238,6 @@ export class LinearElementEditor {
|
|||
const dX = prevCenterX - nextCenterX;
|
||||
const dY = prevCenterY - nextCenterY;
|
||||
const rotated = rotate(offsetX, offsetY, dX, dY, element.angle);
|
||||
|
||||
mutateElement(element, {
|
||||
...otherUpdates,
|
||||
points: nextPoints,
|
||||
|
@ -1258,6 +1272,207 @@ export class LinearElementEditor {
|
|||
|
||||
return rotatePoint([width, height], [0, 0], -element.angle);
|
||||
}
|
||||
|
||||
static getBoundTextElementPosition = (
|
||||
element: ExcalidrawLinearElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
): { x: number; y: number } => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
if (points.length < 2) {
|
||||
mutateElement(boundTextElement, { isDeleted: true });
|
||||
}
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
if (element.points.length % 2 === 1) {
|
||||
const index = Math.floor(element.points.length / 2);
|
||||
const midPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
element.points[index],
|
||||
);
|
||||
x = midPoint[0] - boundTextElement.width / 2;
|
||||
y = midPoint[1] - boundTextElement.height / 2;
|
||||
} else {
|
||||
const index = element.points.length / 2 - 1;
|
||||
|
||||
let midSegmentMidpoint = editorMidPointsCache.points[index];
|
||||
if (element.points.length === 2) {
|
||||
midSegmentMidpoint = centerPoint(points[0], points[1]);
|
||||
}
|
||||
if (
|
||||
!midSegmentMidpoint ||
|
||||
editorMidPointsCache.version !== element.version
|
||||
) {
|
||||
midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint(
|
||||
element,
|
||||
points[index],
|
||||
points[index + 1],
|
||||
index + 1,
|
||||
);
|
||||
}
|
||||
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
|
||||
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
|
||||
}
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
static getMinMaxXYWithBoundText = (
|
||||
element: ExcalidrawLinearElement,
|
||||
elementBounds: [number, number, number, number],
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
): [number, number, number, number, number, number] => {
|
||||
let [x1, y1, x2, y2] = elementBounds;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const { x: boundTextX1, y: boundTextY1 } =
|
||||
LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElement,
|
||||
);
|
||||
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
||||
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
||||
|
||||
const topLeftRotatedPoint = rotatePoint([x1, y1], [cx, cy], element.angle);
|
||||
const topRightRotatedPoint = rotatePoint([x2, y1], [cx, cy], element.angle);
|
||||
|
||||
const counterRotateBoundTextTopLeft = rotatePoint(
|
||||
[boundTextX1, boundTextY1],
|
||||
|
||||
[cx, cy],
|
||||
|
||||
-element.angle,
|
||||
);
|
||||
const counterRotateBoundTextTopRight = rotatePoint(
|
||||
[boundTextX2, boundTextY1],
|
||||
|
||||
[cx, cy],
|
||||
|
||||
-element.angle,
|
||||
);
|
||||
const counterRotateBoundTextBottomLeft = rotatePoint(
|
||||
[boundTextX1, boundTextY2],
|
||||
|
||||
[cx, cy],
|
||||
|
||||
-element.angle,
|
||||
);
|
||||
const counterRotateBoundTextBottomRight = rotatePoint(
|
||||
[boundTextX2, boundTextY2],
|
||||
|
||||
[cx, cy],
|
||||
|
||||
-element.angle,
|
||||
);
|
||||
|
||||
if (
|
||||
topLeftRotatedPoint[0] < topRightRotatedPoint[0] &&
|
||||
topLeftRotatedPoint[1] >= topRightRotatedPoint[1]
|
||||
) {
|
||||
x1 = Math.min(x1, counterRotateBoundTextBottomLeft[0]);
|
||||
x2 = Math.max(
|
||||
x2,
|
||||
Math.max(
|
||||
counterRotateBoundTextTopRight[0],
|
||||
counterRotateBoundTextBottomRight[0],
|
||||
),
|
||||
);
|
||||
y1 = Math.min(y1, counterRotateBoundTextTopLeft[1]);
|
||||
|
||||
y2 = Math.max(y2, counterRotateBoundTextBottomRight[1]);
|
||||
} else if (
|
||||
topLeftRotatedPoint[0] >= topRightRotatedPoint[0] &&
|
||||
topLeftRotatedPoint[1] > topRightRotatedPoint[1]
|
||||
) {
|
||||
x1 = Math.min(x1, counterRotateBoundTextBottomRight[0]);
|
||||
x2 = Math.max(
|
||||
x2,
|
||||
Math.max(
|
||||
counterRotateBoundTextTopLeft[0],
|
||||
counterRotateBoundTextTopRight[0],
|
||||
),
|
||||
);
|
||||
y1 = Math.min(y1, counterRotateBoundTextBottomLeft[1]);
|
||||
|
||||
y2 = Math.max(y2, counterRotateBoundTextTopRight[1]);
|
||||
} else if (topLeftRotatedPoint[0] >= topRightRotatedPoint[0]) {
|
||||
x1 = Math.min(x1, counterRotateBoundTextTopRight[0]);
|
||||
x2 = Math.max(x2, counterRotateBoundTextBottomLeft[0]);
|
||||
y1 = Math.min(y1, counterRotateBoundTextBottomRight[1]);
|
||||
|
||||
y2 = Math.max(y2, counterRotateBoundTextTopLeft[1]);
|
||||
} else if (topLeftRotatedPoint[1] <= topRightRotatedPoint[1]) {
|
||||
x1 = Math.min(
|
||||
x1,
|
||||
Math.min(
|
||||
counterRotateBoundTextTopRight[0],
|
||||
counterRotateBoundTextTopLeft[0],
|
||||
),
|
||||
);
|
||||
|
||||
x2 = Math.max(x2, counterRotateBoundTextBottomRight[0]);
|
||||
y1 = Math.min(y1, counterRotateBoundTextTopRight[1]);
|
||||
y2 = Math.max(y2, counterRotateBoundTextBottomLeft[1]);
|
||||
}
|
||||
|
||||
return [x1, y1, x2, y2, cx, cy];
|
||||
};
|
||||
|
||||
static getElementAbsoluteCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
let coords: [number, number, number, number, number, number];
|
||||
let x1;
|
||||
let y1;
|
||||
let x2;
|
||||
let y2;
|
||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||
// XXX this is just a poor estimate and not very useful
|
||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||
(limits, [x, y]) => {
|
||||
limits.minY = Math.min(limits.minY, y);
|
||||
limits.minX = Math.min(limits.minX, x);
|
||||
|
||||
limits.maxX = Math.max(limits.maxX, x);
|
||||
limits.maxY = Math.max(limits.maxY, y);
|
||||
|
||||
return limits;
|
||||
},
|
||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||
);
|
||||
x1 = minX + element.x;
|
||||
y1 = minY + element.y;
|
||||
x2 = maxX + element.x;
|
||||
y2 = maxY + element.y;
|
||||
} else {
|
||||
const shape = getShapeForElement(element)!;
|
||||
|
||||
// first element is always the curve
|
||||
const ops = getCurvePathOps(shape[0]);
|
||||
|
||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||
x1 = minX + element.x;
|
||||
y1 = minY + element.y;
|
||||
x2 = maxX + element.x;
|
||||
y2 = maxY + element.y;
|
||||
}
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
coords = [x1, y1, x2, y2, cx, cy];
|
||||
|
||||
if (!includeBoundText) {
|
||||
return coords;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
coords = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
[x1, y1, x2, y2],
|
||||
boundTextElement,
|
||||
);
|
||||
}
|
||||
|
||||
return coords;
|
||||
};
|
||||
}
|
||||
|
||||
const normalizeSelectedPoints = (
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
FontFamilyValues,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawTextContainer,
|
||||
} from "../element/types";
|
||||
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
|
@ -22,6 +22,8 @@ import { getElementAbsoluteCoords } from ".";
|
|||
import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getBoundTextElementOffset,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
|
@ -29,6 +31,7 @@ import {
|
|||
wrapText,
|
||||
} from "./textElement";
|
||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||
import { isArrowElement } from "./typeChecks";
|
||||
|
||||
type ElementConstructorOpts = MarkOptional<
|
||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||
|
@ -131,7 +134,7 @@ export const newTextElement = (
|
|||
fontFamily: FontFamilyValues;
|
||||
textAlign: TextAlign;
|
||||
verticalAlign: VerticalAlign;
|
||||
containerId?: ExcalidrawRectangleElement["id"];
|
||||
containerId?: ExcalidrawTextContainer["id"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> => {
|
||||
const text = normalizeText(opts.text);
|
||||
|
@ -231,16 +234,21 @@ const getAdjustedDimensions = (
|
|||
// make sure container dimensions are set properly when
|
||||
// text editor overflows beyond viewport dimensions
|
||||
if (container) {
|
||||
const boundTextElementPadding = getBoundTextElementOffset(element);
|
||||
|
||||
const containerDims = getContainerDims(container);
|
||||
let height = containerDims.height;
|
||||
let width = containerDims.width;
|
||||
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
||||
height = nextHeight + BOUND_TEXT_PADDING * 2;
|
||||
if (nextHeight > height - boundTextElementPadding * 2) {
|
||||
height = nextHeight + boundTextElementPadding * 2;
|
||||
}
|
||||
if (nextWidth > width - BOUND_TEXT_PADDING * 2) {
|
||||
width = nextWidth + BOUND_TEXT_PADDING * 2;
|
||||
if (nextWidth > width - boundTextElementPadding * 2) {
|
||||
width = nextWidth + boundTextElementPadding * 2;
|
||||
}
|
||||
if (height !== containerDims.height || width !== containerDims.width) {
|
||||
if (
|
||||
!isArrowElement(container) &&
|
||||
(height !== containerDims.height || width !== containerDims.width)
|
||||
) {
|
||||
mutateElement(container, { height, width });
|
||||
}
|
||||
}
|
||||
|
@ -270,11 +278,35 @@ export const refreshTextDimensions = (
|
|||
};
|
||||
|
||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||
return getContainerDims(container).width - BOUND_TEXT_PADDING * 2;
|
||||
const width = getContainerDims(container).width;
|
||||
if (isArrowElement(container)) {
|
||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerWidth <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.width;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return containerWidth;
|
||||
}
|
||||
return width - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||
return getContainerDims(container).height - BOUND_TEXT_PADDING * 2;
|
||||
const height = getContainerDims(container).height;
|
||||
if (isArrowElement(container)) {
|
||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerHeight <= 0) {
|
||||
const boundText = getBoundTextElement(container);
|
||||
if (boundText) {
|
||||
return boundText.height;
|
||||
}
|
||||
return BOUND_TEXT_PADDING * 8 * 2;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
return height - BOUND_TEXT_PADDING * 2;
|
||||
};
|
||||
|
||||
export const updateTextElement = (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BOUND_TEXT_PADDING, SHIFT_LOCKING_ANGLE } from "../constants";
|
||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||
import { rescalePoints } from "../points";
|
||||
|
||||
import {
|
||||
|
@ -12,6 +12,8 @@ import {
|
|||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
} from "./types";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
|
@ -20,6 +22,7 @@ import {
|
|||
getCommonBoundingBox,
|
||||
} from "./bounds";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFreeDrawElement,
|
||||
isLinearElement,
|
||||
|
@ -40,6 +43,7 @@ import {
|
|||
getApproxMinLineWidth,
|
||||
getBoundTextElement,
|
||||
getBoundTextElementId,
|
||||
getBoundTextElementOffset,
|
||||
getContainerElement,
|
||||
handleBindTextResize,
|
||||
measureText,
|
||||
|
@ -75,6 +79,7 @@ export const transformElements = (
|
|||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
pointerDownState.originalElements,
|
||||
);
|
||||
updateBoundElements(element);
|
||||
} else if (
|
||||
|
@ -142,6 +147,7 @@ const rotateSingleElement = (
|
|||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
|
@ -152,11 +158,17 @@ const rotateSingleElement = (
|
|||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
angle = normalizeAngle(angle);
|
||||
mutateElement(element, { angle });
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
mutateElement(element, { angle });
|
||||
if (boundTextElementId) {
|
||||
const textElement = Scene.getScene(element)!.getElement(boundTextElementId);
|
||||
mutateElement(textElement!, { angle });
|
||||
const textElement = Scene.getScene(element)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElementWithContainer;
|
||||
|
||||
if (!isArrowElement(element)) {
|
||||
mutateElement(textElement, { angle });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -412,10 +424,12 @@ export const resizeSingleElement = (
|
|||
};
|
||||
}
|
||||
if (shouldMaintainAspectRatio) {
|
||||
const boundTextElementPadding =
|
||||
getBoundTextElementOffset(boundTextElement);
|
||||
const nextFont = measureFontSizeFromWH(
|
||||
boundTextElement,
|
||||
eleNewWidth - BOUND_TEXT_PADDING * 2,
|
||||
eleNewHeight - BOUND_TEXT_PADDING * 2,
|
||||
eleNewWidth - boundTextElementPadding * 2,
|
||||
eleNewHeight - boundTextElementPadding * 2,
|
||||
);
|
||||
if (nextFont === null) {
|
||||
return;
|
||||
|
@ -504,24 +518,36 @@ export const resizeSingleElement = (
|
|||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||
|
||||
// Readjust points for linear elements
|
||||
const rescaledPoints = rescalePointsInElement(
|
||||
stateAtResizeStart,
|
||||
eleNewWidth,
|
||||
eleNewHeight,
|
||||
true,
|
||||
);
|
||||
let rescaledElementPointsY;
|
||||
let rescaledPoints;
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
rescaledElementPointsY = rescalePoints(
|
||||
1,
|
||||
eleNewHeight,
|
||||
(stateAtResizeStart as ExcalidrawLinearElement).points,
|
||||
true,
|
||||
);
|
||||
|
||||
rescaledPoints = rescalePoints(
|
||||
0,
|
||||
eleNewWidth,
|
||||
rescaledElementPointsY,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||
// So we need to readjust (x,y) to be where the first point should be
|
||||
const newOrigin = [...newTopLeft];
|
||||
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
|
||||
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
|
||||
|
||||
const resizedElement = {
|
||||
width: Math.abs(eleNewWidth),
|
||||
height: Math.abs(eleNewHeight),
|
||||
x: newOrigin[0],
|
||||
y: newOrigin[1],
|
||||
...rescaledPoints,
|
||||
points: rescaledPoints,
|
||||
};
|
||||
|
||||
if ("scale" in element && "scale" in stateAtResizeStart) {
|
||||
|
@ -545,6 +571,7 @@ export const resizeSingleElement = (
|
|||
updateBoundElements(element, {
|
||||
newSize: { width: resizedElement.width, height: resizedElement.height },
|
||||
});
|
||||
|
||||
mutateElement(element, resizedElement);
|
||||
if (boundTextElement && boundTextFont) {
|
||||
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
||||
|
@ -667,7 +694,7 @@ const resizeMultipleElements = (
|
|||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
|
||||
if (boundTextElement || isTextElement(element.orig)) {
|
||||
const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
|
||||
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2;
|
||||
const textMeasurements = measureFontSizeFromWH(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
width - optionalPadding,
|
||||
|
@ -697,6 +724,7 @@ const resizeMultipleElements = (
|
|||
|
||||
if (boundTextElement && boundTextUpdates) {
|
||||
mutateElement(boundTextElement, boundTextUpdates);
|
||||
|
||||
handleBindTextResize(element.latest, transformHandleType);
|
||||
}
|
||||
});
|
||||
|
@ -717,7 +745,7 @@ const rotateMultipleElements = (
|
|||
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
||||
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
||||
}
|
||||
elements.forEach((element, index) => {
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
|
@ -737,13 +765,16 @@ const rotateMultipleElements = (
|
|||
});
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
if (boundTextElementId) {
|
||||
const textElement =
|
||||
Scene.getScene(element)!.getElement(boundTextElementId)!;
|
||||
mutateElement(textElement, {
|
||||
x: textElement.x + (rotatedCX - cx),
|
||||
y: textElement.y + (rotatedCY - cy),
|
||||
angle: normalizeAngle(centerAngle + origAngle),
|
||||
});
|
||||
const textElement = Scene.getScene(element)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElementWithContainer;
|
||||
if (!isArrowElement(element)) {
|
||||
mutateElement(textElement, {
|
||||
x: textElement.x + (rotatedCX - cx),
|
||||
y: textElement.y + (rotatedCY - cy),
|
||||
angle: normalizeAngle(centerAngle + origAngle),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -94,7 +94,7 @@ export const getTransformHandleTypeFromCoords = (
|
|||
pointerType: PointerType,
|
||||
): MaybeTransformHandleType => {
|
||||
const transformHandles = getTransformHandlesFromCoords(
|
||||
[x1, y1, x2, y2],
|
||||
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
||||
0,
|
||||
zoom,
|
||||
pointerType,
|
||||
|
|
|
@ -13,11 +13,17 @@ import { MaybeTransformHandleType } from "./transformHandles";
|
|||
import Scene from "../scene/Scene";
|
||||
import { isTextElement } from ".";
|
||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isImageElement,
|
||||
isArrowElement,
|
||||
} from "./typeChecks";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { AppState } from "../types";
|
||||
import { isTextBindableContainer } from "./typeChecks";
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
import { AppState } from "../types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isImageElement } from "./typeChecks";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
|
@ -52,36 +58,47 @@ export const redrawTextBoundingBox = (
|
|||
let coordX = textElement.x;
|
||||
// Resize container and vertically center align the text
|
||||
if (container) {
|
||||
const containerDims = getContainerDims(container);
|
||||
let nextHeight = containerDims.height;
|
||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
coordY = container.y + BOUND_TEXT_PADDING;
|
||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
coordY =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
metrics.height -
|
||||
BOUND_TEXT_PADDING;
|
||||
} else {
|
||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
||||
if (metrics.height > getMaxContainerHeight(container)) {
|
||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||
if (!isArrowElement(container)) {
|
||||
const containerDims = getContainerDims(container);
|
||||
let nextHeight = containerDims.height;
|
||||
const boundTextElementPadding = getBoundTextElementOffset(textElement);
|
||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
coordY = container.y + boundTextElementPadding;
|
||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
coordY =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
metrics.height -
|
||||
boundTextElementPadding;
|
||||
} else {
|
||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
||||
if (metrics.height > getMaxContainerHeight(container)) {
|
||||
nextHeight = metrics.height + boundTextElementPadding * 2;
|
||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||
}
|
||||
}
|
||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||
coordX = container.x + boundTextElementPadding;
|
||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
coordX =
|
||||
container.x +
|
||||
containerDims.width -
|
||||
metrics.width -
|
||||
boundTextElementPadding;
|
||||
} else {
|
||||
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||
coordX = container.x + BOUND_TEXT_PADDING;
|
||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||
coordX =
|
||||
container.x + containerDims.width - metrics.width - BOUND_TEXT_PADDING;
|
||||
mutateElement(container, { height: nextHeight });
|
||||
} else {
|
||||
coordX = container.x + container.width / 2 - metrics.width / 2;
|
||||
const centerX = textElement.x + textElement.width / 2;
|
||||
const centerY = textElement.y + textElement.height / 2;
|
||||
const diffWidth = metrics.width - textElement.width;
|
||||
const diffHeight = metrics.height - textElement.height;
|
||||
coordY = centerY - (textElement.height + diffHeight) / 2;
|
||||
coordX = centerX - (textElement.width + diffWidth) / 2;
|
||||
}
|
||||
|
||||
mutateElement(container, { height: nextHeight });
|
||||
}
|
||||
|
||||
mutateElement(textElement, {
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
|
@ -129,84 +146,113 @@ export const bindTextToShapeAfterDuplication = (
|
|||
};
|
||||
|
||||
export const handleBindTextResize = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
container: NonDeletedExcalidrawElement,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
) => {
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
if (boundTextElementId) {
|
||||
const textElement = Scene.getScene(element)!.getElement(
|
||||
const boundTextElementId = getBoundTextElementId(container);
|
||||
if (!boundTextElementId) {
|
||||
return;
|
||||
}
|
||||
let textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
if (textElement && textElement.text) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
if (textElement && textElement.text) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
let text = textElement.text;
|
||||
let nextHeight = textElement.height;
|
||||
let nextWidth = textElement.width;
|
||||
let containerHeight = element.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
getMaxContainerWidth(element),
|
||||
);
|
||||
}
|
||||
|
||||
const dimensions = measureText(
|
||||
text,
|
||||
let text = textElement.text;
|
||||
let nextHeight = textElement.height;
|
||||
let nextWidth = textElement.width;
|
||||
const containerDims = getContainerDims(container);
|
||||
const maxWidth = getMaxContainerWidth(container);
|
||||
const maxHeight = getMaxContainerHeight(container);
|
||||
let containerHeight = containerDims.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
element.width,
|
||||
maxWidth,
|
||||
);
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
nextBaseLine = dimensions.baseline;
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
|
||||
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
|
||||
const diff = containerHeight - element.height;
|
||||
// fix the y coord when resizing from ne/nw/n
|
||||
const updatedY =
|
||||
transformHandleType === "ne" ||
|
||||
transformHandleType === "nw" ||
|
||||
transformHandleType === "n"
|
||||
? element.y - diff
|
||||
: element.y;
|
||||
mutateElement(element, {
|
||||
height: containerHeight,
|
||||
y: updatedY,
|
||||
});
|
||||
}
|
||||
|
||||
let updatedY;
|
||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
updatedY = element.y + BOUND_TEXT_PADDING;
|
||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
updatedY = element.y + element.height - nextHeight - BOUND_TEXT_PADDING;
|
||||
} else {
|
||||
updatedY = element.y + element.height / 2 - nextHeight / 2;
|
||||
}
|
||||
const updatedX =
|
||||
textElement.textAlign === TEXT_ALIGN.LEFT
|
||||
? element.x + BOUND_TEXT_PADDING
|
||||
: textElement.textAlign === TEXT_ALIGN.RIGHT
|
||||
? element.x + element.width - nextWidth - BOUND_TEXT_PADDING
|
||||
: element.x + element.width / 2 - nextWidth / 2;
|
||||
mutateElement(textElement, {
|
||||
const dimensions = measureText(
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
x: updatedX,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
nextBaseLine = dimensions.baseline;
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > maxHeight) {
|
||||
containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2;
|
||||
const diff = containerHeight - containerDims.height;
|
||||
// fix the y coord when resizing from ne/nw/n
|
||||
const updatedY =
|
||||
!isArrowElement(container) &&
|
||||
(transformHandleType === "ne" ||
|
||||
transformHandleType === "nw" ||
|
||||
transformHandleType === "n")
|
||||
? container.y - diff
|
||||
: container.y;
|
||||
mutateElement(container, {
|
||||
height: containerHeight,
|
||||
y: updatedY,
|
||||
baseline: nextBaseLine,
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(textElement, {
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
|
||||
baseline: nextBaseLine,
|
||||
});
|
||||
if (!isArrowElement(container)) {
|
||||
updateBoundTextPosition(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateBoundTextPosition = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
) => {
|
||||
const containerDims = getContainerDims(container);
|
||||
const boundTextElementPadding = getBoundTextElementOffset(boundTextElement);
|
||||
let y;
|
||||
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||
y = container.y + boundTextElementPadding;
|
||||
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
y =
|
||||
container.y +
|
||||
containerDims.height -
|
||||
boundTextElement.height -
|
||||
boundTextElementPadding;
|
||||
} else {
|
||||
y = container.y + containerDims.height / 2 - boundTextElement.height / 2;
|
||||
}
|
||||
const x =
|
||||
boundTextElement.textAlign === TEXT_ALIGN.LEFT
|
||||
? container.x + boundTextElementPadding
|
||||
: boundTextElement.textAlign === TEXT_ALIGN.RIGHT
|
||||
? container.x +
|
||||
containerDims.width -
|
||||
boundTextElement.width -
|
||||
boundTextElementPadding
|
||||
: container.x + containerDims.width / 2 - boundTextElement.width / 2;
|
||||
|
||||
mutateElement(boundTextElement, { x, y });
|
||||
};
|
||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||
export const measureText = (
|
||||
text: string,
|
||||
|
@ -411,6 +457,7 @@ export const charWidth = (() => {
|
|||
})();
|
||||
export const getApproxMinLineWidth = (font: FontString) => {
|
||||
const maxCharWidth = getMaxCharWidth(font);
|
||||
|
||||
if (maxCharWidth === 0) {
|
||||
return (
|
||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||
|
@ -491,7 +538,9 @@ export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
|||
|
||||
export const getContainerElement = (
|
||||
element:
|
||||
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
|
||||
| (ExcalidrawElement & {
|
||||
containerId: ExcalidrawElement["id"] | null;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!element) {
|
||||
|
@ -504,9 +553,106 @@ export const getContainerElement = (
|
|||
};
|
||||
|
||||
export const getContainerDims = (element: ExcalidrawElement) => {
|
||||
const MIN_WIDTH = 300;
|
||||
if (isArrowElement(element)) {
|
||||
const width = Math.max(element.width, MIN_WIDTH);
|
||||
const height = element.height;
|
||||
return { width, height };
|
||||
}
|
||||
return { width: element.width, height: element.height };
|
||||
};
|
||||
|
||||
export const getContainerCenter = (
|
||||
container: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (!isArrowElement(container)) {
|
||||
return {
|
||||
x: container.x + container.width / 2,
|
||||
y: container.y + container.height / 2,
|
||||
};
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(container);
|
||||
if (points.length % 2 === 1) {
|
||||
const index = Math.floor(container.points.length / 2);
|
||||
const midPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||
container,
|
||||
container.points[index],
|
||||
);
|
||||
return { x: midPoint[0], y: midPoint[1] };
|
||||
}
|
||||
const index = container.points.length / 2 - 1;
|
||||
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
||||
container,
|
||||
appState,
|
||||
)[index];
|
||||
if (!midSegmentMidpoint) {
|
||||
midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint(
|
||||
container,
|
||||
points[index],
|
||||
points[index + 1],
|
||||
index + 1,
|
||||
);
|
||||
}
|
||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||
};
|
||||
|
||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||
const container = getContainerElement(textElement);
|
||||
if (!container || isArrowElement(container)) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
};
|
||||
|
||||
export const getBoundTextElementOffset = (
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const container = getContainerElement(boundTextElement);
|
||||
if (!container) {
|
||||
return 0;
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return BOUND_TEXT_PADDING * 8;
|
||||
}
|
||||
return BOUND_TEXT_PADDING;
|
||||
};
|
||||
|
||||
export const getBoundTextElementPosition = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
) => {
|
||||
if (isArrowElement(container)) {
|
||||
return LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
boundTextElement,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const shouldAllowVerticalAlign = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
) => {
|
||||
return selectedElements.some((element) => {
|
||||
const hasBoundContainer = isBoundToContainer(element);
|
||||
if (hasBoundContainer) {
|
||||
const container = getContainerElement(element);
|
||||
if (isTextElement(element) && isArrowElement(container)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
if (isArrowElement(element)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
export const getTextBindableContainerAtPosition = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
|
@ -515,7 +661,9 @@ export const getTextBindableContainerAtPosition = (
|
|||
): ExcalidrawTextContainer | null => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
if (selectedElements.length === 1) {
|
||||
return selectedElements[0] as ExcalidrawTextContainer;
|
||||
return isTextBindableContainer(selectedElements[0], false)
|
||||
? selectedElements[0]
|
||||
: null;
|
||||
}
|
||||
let hitElement = null;
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
|
@ -524,7 +672,16 @@ export const getTextBindableContainerAtPosition = (
|
|||
continue;
|
||||
}
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
||||
if (x1 < x && x < x2 && y1 < y && y < y2) {
|
||||
if (
|
||||
isArrowElement(elements[index]) &&
|
||||
isHittingElementNotConsideringBoundingBox(elements[index], appState, [
|
||||
x,
|
||||
y,
|
||||
])
|
||||
) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
} else if (x1 < x && x < x2 && y1 < y && y < y2) {
|
||||
hitElement = elements[index];
|
||||
break;
|
||||
}
|
||||
|
@ -538,6 +695,7 @@ export const isValidTextContainer = (element: ExcalidrawElement) => {
|
|||
element.type === "rectangle" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "diamond" ||
|
||||
isImageElement(element)
|
||||
isImageElement(element) ||
|
||||
isArrowElement(element)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -513,6 +513,9 @@ describe("textWysiwyg", () => {
|
|||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
mouse.down();
|
||||
const editor = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
|
@ -586,20 +589,19 @@ describe("textWysiwyg", () => {
|
|||
});
|
||||
|
||||
it("shouldn't bind to non-text-bindable containers", async () => {
|
||||
const line = API.createElement({
|
||||
type: "line",
|
||||
const freedraw = API.createElement({
|
||||
type: "freedraw",
|
||||
width: 100,
|
||||
height: 0,
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
],
|
||||
});
|
||||
h.elements = [line];
|
||||
h.elements = [freedraw];
|
||||
|
||||
UI.clickTool("text");
|
||||
|
||||
mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
|
||||
mouse.clickAt(
|
||||
freedraw.x + freedraw.width / 2,
|
||||
freedraw.y + freedraw.height / 2,
|
||||
);
|
||||
|
||||
const editor = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
|
@ -613,20 +615,22 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||
editor.dispatchEvent(new Event("input"));
|
||||
|
||||
expect(line.boundElements).toBe(null);
|
||||
expect(freedraw.boundElements).toBe(null);
|
||||
expect(h.elements[1].type).toBe("text");
|
||||
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
|
||||
});
|
||||
|
||||
it("shouldn't create text element when pressing 'Enter' key on non text bindable container", async () => {
|
||||
h.elements = [];
|
||||
const freeDraw = UI.createElement("freedraw", {
|
||||
width: 100,
|
||||
height: 50,
|
||||
["freedraw", "line"].forEach((type: any) => {
|
||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||
h.elements = [];
|
||||
const elemnet = UI.createElement(type, {
|
||||
width: 100,
|
||||
height: 50,
|
||||
});
|
||||
API.setSelectedElements([elemnet]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.elements.length).toBe(1);
|
||||
});
|
||||
API.setSelectedElements([freeDraw]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.elements.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should'nt bind text to container when not double clicked on center", async () => {
|
||||
|
@ -1206,7 +1210,7 @@ describe("textWysiwyg", () => {
|
|||
|
||||
fireEvent.change(editor, { target: { value: " " } });
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toBeNull();
|
||||
expect(rectangle.boundElements).toStrictEqual([]);
|
||||
expect(h.elements[1].isDeleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,16 @@ import {
|
|||
isTestEnv,
|
||||
} from "../utils";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
||||
import { CLASSES, BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawTextElement,
|
||||
} from "./types";
|
||||
import { AppState } from "../types";
|
||||
|
@ -18,8 +23,10 @@ import { mutateElement } from "./mutateElement";
|
|||
import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElementId,
|
||||
getBoundTextElementOffset,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
getTextElementAngle,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
|
@ -30,7 +37,8 @@ import {
|
|||
} from "../actions/actionProperties";
|
||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
|
||||
const getTransform = (
|
||||
|
@ -108,7 +116,7 @@ export const textWysiwyg = ({
|
|||
getFontString(updatedTextElement),
|
||||
);
|
||||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||
const coordX = updatedTextElement.x;
|
||||
let coordX = updatedTextElement.x;
|
||||
let coordY = updatedTextElement.y;
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
@ -119,6 +127,15 @@ export const textWysiwyg = ({
|
|||
// what is going to be used for unbounded text
|
||||
let height = updatedTextElement.height;
|
||||
if (container && updatedTextElement.containerId) {
|
||||
if (isArrowElement(container)) {
|
||||
const boundTextCoords =
|
||||
LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
coordX = boundTextCoords.x;
|
||||
coordY = boundTextCoords.y;
|
||||
}
|
||||
const propertiesUpdated = textPropertiesUpdated(
|
||||
updatedTextElement,
|
||||
editable,
|
||||
|
@ -138,16 +155,19 @@ export const textWysiwyg = ({
|
|||
if (!originalContainerHeight) {
|
||||
originalContainerHeight = containerDims.height;
|
||||
}
|
||||
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
||||
maxHeight = containerDims.height - BOUND_TEXT_PADDING * 2;
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
maxHeight = getMaxContainerHeight(container);
|
||||
|
||||
// autogrow container height if text exceeds
|
||||
if (height > maxHeight) {
|
||||
|
||||
if (!isArrowElement(container) && height > maxHeight) {
|
||||
const diff = Math.min(height - maxHeight, approxLineHeight);
|
||||
mutateElement(container, { height: containerDims.height + diff });
|
||||
return;
|
||||
} else if (
|
||||
// autoshrink container height until original container height
|
||||
// is reached when text is removed
|
||||
!isArrowElement(container) &&
|
||||
containerDims.height > originalContainerHeight &&
|
||||
height < maxHeight
|
||||
) {
|
||||
|
@ -159,11 +179,16 @@ export const textWysiwyg = ({
|
|||
else {
|
||||
// vertically center align the text
|
||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||
coordY = container.y + containerDims.height / 2 - height / 2;
|
||||
if (!isArrowElement(container)) {
|
||||
coordY = container.y + containerDims.height / 2 - height / 2;
|
||||
}
|
||||
}
|
||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
coordY =
|
||||
container.y + containerDims.height - height - BOUND_TEXT_PADDING;
|
||||
container.y +
|
||||
containerDims.height -
|
||||
height -
|
||||
getBoundTextElementOffset(updatedTextElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,7 +222,7 @@ export const textWysiwyg = ({
|
|||
// Make sure text editor height doesn't go beyond viewport
|
||||
const editorMaxHeight =
|
||||
(appState.height - viewportY) / appState.zoom.value;
|
||||
const angle = container ? container.angle : updatedTextElement.angle;
|
||||
|
||||
Object.assign(editable.style, {
|
||||
font: getFontString(updatedTextElement),
|
||||
// must be defined *after* font ¯\_(ツ)_/¯
|
||||
|
@ -209,7 +234,7 @@ export const textWysiwyg = ({
|
|||
transform: getTransform(
|
||||
width,
|
||||
height,
|
||||
angle,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
appState,
|
||||
maxWidth,
|
||||
editorMaxHeight,
|
||||
|
@ -246,6 +271,8 @@ export const textWysiwyg = ({
|
|||
whiteSpace = "pre-wrap";
|
||||
wordBreak = "break-word";
|
||||
}
|
||||
const isContainerArrow = isArrowElement(getContainerElement(element));
|
||||
const background = isContainerArrow ? "#fff" : "transparent";
|
||||
Object.assign(editable.style, {
|
||||
position: "absolute",
|
||||
display: "inline-block",
|
||||
|
@ -256,7 +283,7 @@ export const textWysiwyg = ({
|
|||
border: 0,
|
||||
outline: 0,
|
||||
resize: "none",
|
||||
background: "transparent",
|
||||
background,
|
||||
overflow: "hidden",
|
||||
// must be specified because in dark mode canvas creates a stacking context
|
||||
zIndex: "var(--zIndex-wysiwyg)",
|
||||
|
@ -264,6 +291,7 @@ export const textWysiwyg = ({
|
|||
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
|
||||
whiteSpace,
|
||||
overflowWrap: "break-word",
|
||||
boxSizing: "content-box",
|
||||
});
|
||||
updateWysiwygStyle();
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
PointerType,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords, Bounds } from "./bounds";
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
import { rotate } from "../math";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
|
@ -81,7 +81,7 @@ const generateTransformHandle = (
|
|||
};
|
||||
|
||||
export const getTransformHandlesFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
[x1, y1, x2, y2, cx, cy]: [number, number, number, number, number, number],
|
||||
angle: number,
|
||||
zoom: Zoom,
|
||||
pointerType: PointerType,
|
||||
|
@ -97,8 +97,6 @@ export const getTransformHandlesFromCoords = (
|
|||
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const dashedLineMargin = margin / zoom.value;
|
||||
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||
|
||||
|
@ -256,7 +254,7 @@ export const getTransformHandles = (
|
|||
? DEFAULT_SPACING + 8
|
||||
: DEFAULT_SPACING;
|
||||
return getTransformHandlesFromCoords(
|
||||
getElementAbsoluteCoords(element),
|
||||
getElementAbsoluteCoords(element, true),
|
||||
element.angle,
|
||||
zoom,
|
||||
pointerType,
|
||||
|
|
|
@ -60,6 +60,12 @@ export const isLinearElement = (
|
|||
return element != null && isLinearElementType(element.type);
|
||||
};
|
||||
|
||||
export const isArrowElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawLinearElement => {
|
||||
return element != null && element.type === "arrow";
|
||||
};
|
||||
|
||||
export const isLinearElementType = (
|
||||
elementType: AppState["activeTool"]["type"],
|
||||
): boolean => {
|
||||
|
@ -110,7 +116,8 @@ export const isTextBindableContainer = (
|
|||
(element.type === "rectangle" ||
|
||||
element.type === "diamond" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "image")
|
||||
element.type === "image" ||
|
||||
isArrowElement(element))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -141,7 +141,8 @@ export type ExcalidrawTextContainer =
|
|||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawImageElement;
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawArrowEleement;
|
||||
|
||||
export type ExcalidrawTextElementWithContainer = {
|
||||
containerId: ExcalidrawTextContainer["id"];
|
||||
|
@ -166,6 +167,11 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||
endArrowhead: Arrowhead | null;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawArrowEleement = ExcalidrawLinearElement &
|
||||
Readonly<{
|
||||
type: "arrow";
|
||||
}>;
|
||||
|
||||
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "freedraw";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue