{ @@ -218,6 +226,24 @@ export const getEmbedLink = ( return ret; } + if (RE_REDDIT.test(link)) { + const [, page, postId, title] = link.match(RE_REDDIT)!; + const safeURL = sanitizeHTMLAttribute( + `https://reddit.com/r/${page}/comments/${postId}/${title}`, + ); + const ret: IframeDataWithSandbox = { + type: "document", + srcdoc: (theme: string) => + createSrcDoc( + ``, + ), + intrinsicSize: { w: 480, h: 480 }, + sandbox: { allowSameOrigin }, + }; + embeddedLinkCache.set(originalLink, ret); + return ret; + } + if (RE_GH_GIST.test(link)) { const [, user, gistId] = link.match(RE_GH_GIST)!; const safeURL = sanitizeHTMLAttribute( @@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => { return twitterMatch[1]; } + const redditMatch = str.match(RE_REDDIT_EMBED); + if (redditMatch && redditMatch.length === 2) { + return redditMatch[1]; + } + const gistMatch = str.match(RE_GH_GIST_EMBED); if (gistMatch && gistMatch.length === 2) { return gistMatch[1]; diff --git a/packages/excalidraw/element/flowchart.ts b/packages/excalidraw/element/flowchart.ts index cc174bfa9..8c14bc01a 100644 --- a/packages/excalidraw/element/flowchart.ts +++ b/packages/excalidraw/element/flowchart.ts @@ -29,7 +29,7 @@ import { isFlowchartNodeElement, } from "./typeChecks"; import { invariant } from "../utils"; -import { point, type LocalPoint } from "../../math"; +import { pointFrom, type LocalPoint } from "../../math"; import { aabbForElement } from "../shapes"; type LinkDirection = "up" | "right" | "down" | "left"; @@ -421,7 +421,7 @@ const createBindingArrow = ( strokeColor: appState.currentItemStrokeColor, strokeStyle: appState.currentItemStrokeStyle, strokeWidth: appState.currentItemStrokeWidth, - points: [point(0, 0), point(endX, endY)], + points: [pointFrom(0, 0), pointFrom(endX, endY)], elbowed: true, }); diff --git a/packages/excalidraw/element/heading.ts b/packages/excalidraw/element/heading.ts index b22316c6a..c17a077fc 100644 --- a/packages/excalidraw/element/heading.ts +++ b/packages/excalidraw/element/heading.ts @@ -6,7 +6,7 @@ import type { Radians, } from "../../math"; import { - point, + pointFrom, pointRotateRads, pointScaleFromOrigin, radiansToDegrees, @@ -82,7 +82,7 @@ export const headingForPointFromElement = < const top = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width / 2, element.y), + pointFrom(element.x + element.width / 2, element.y), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -91,7 +91,7 @@ export const headingForPointFromElement = < ); const right = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width, element.y + element.height / 2), + pointFrom(element.x + element.width, element.y + element.height / 2), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -100,7 +100,7 @@ export const headingForPointFromElement = < ); const bottom = pointRotateRads( pointScaleFromOrigin( - point(element.x + element.width / 2, element.y + element.height), + pointFrom(element.x + element.width / 2, element.y + element.height), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -109,7 +109,7 @@ export const headingForPointFromElement = < ); const left = pointRotateRads( pointScaleFromOrigin( - point(element.x, element.y + element.height / 2), + pointFrom(element.x, element.y + element.height / 2), midPoint, SEARCH_CONE_MULTIPLIER, ), @@ -133,22 +133,22 @@ export const headingForPointFromElement = < } const topLeft = pointScaleFromOrigin( - point(aabb[0], aabb[1]), + pointFrom(aabb[0], aabb[1]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const topRight = pointScaleFromOrigin( - point(aabb[2], aabb[1]), + pointFrom(aabb[2], aabb[1]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const bottomLeft = pointScaleFromOrigin( - point(aabb[0], aabb[3]), + pointFrom(aabb[0], aabb[3]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; const bottomRight = pointScaleFromOrigin( - point(aabb[2], aabb[3]), + pointFrom(aabb[2], aabb[3]), midPoint, SEARCH_CONE_MULTIPLIER, ) as Point; diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index 7607a2e16..0d1db7733 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -49,7 +49,7 @@ import type Scene from "../scene/Scene"; import type { Radians } from "../../math"; import { pointCenter, - point, + pointFrom, pointRotateRads, pointsEqual, vector, @@ -102,12 +102,13 @@ export class LinearElementEditor { public readonly endBindingElement: ExcalidrawBindableElement | null | "keep"; public readonly hoverPointIndex: number; public readonly segmentMidPointHoveredCoords: GlobalPoint | null; + public readonly elbowed: boolean; constructor(element: NonDeleted) { this.elementId = element.id as string & { _brand: "excalidrawLinearElementId"; }; - if (!pointsEqual(element.points[0], point(0, 0))) { + if (!pointsEqual(element.points[0], pointFrom(0, 0))) { console.error("Linear element is not normalized", Error().stack); } @@ -131,6 +132,7 @@ export class LinearElementEditor { }; this.hoverPointIndex = -1; this.segmentMidPointHoveredCoords = null; + this.elbowed = isElbowArrow(element) && element.elbowed; } // --------------------------------------------------------------------------- @@ -285,7 +287,7 @@ export class LinearElementEditor { element, elementsMap, referencePoint, - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); @@ -294,7 +296,7 @@ export class LinearElementEditor { [ { index: selectedIndex, - point: point( + point: pointFrom( width + referencePoint[0], height + referencePoint[1], ), @@ -327,7 +329,7 @@ export class LinearElementEditor { scenePointerY - linearElementEditor.pointerOffset.y, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ) - : point( + : pointFrom( element.points[pointIndex][0] + deltaX, element.points[pointIndex][1] + deltaY, ); @@ -588,11 +590,11 @@ export class LinearElementEditor { linearElementEditor.segmentMidPointHoveredCoords; if (existingSegmentMidpointHitCoords) { const distance = pointDistance( - point( + pointFrom( existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[1], ), - point(scenePointer.x, scenePointer.y), + pointFrom(scenePointer.x, scenePointer.y), ); if (distance <= threshold) { return existingSegmentMidpointHitCoords; @@ -604,8 +606,8 @@ export class LinearElementEditor { while (index < midPoints.length) { if (midPoints[index] !== null) { const distance = pointDistance( - point(midPoints[index]![0], midPoints[index]![1]), - point(scenePointer.x, scenePointer.y), + pointFrom(midPoints[index]![0], midPoints[index]![1]), + pointFrom(scenePointer.x, scenePointer.y), ); if (distance <= threshold) { return midPoints[index]; @@ -624,8 +626,8 @@ export class LinearElementEditor { zoom: AppState["zoom"], ) { let distance = pointDistance( - point(startPoint[0], startPoint[1]), - point(endPoint[0], endPoint[1]), + pointFrom(startPoint[0], startPoint[1]), + pointFrom(endPoint[0], endPoint[1]), ); if (element.points.length > 2 && element.roundness) { distance = getBezierCurveLength(element, endPoint); @@ -827,11 +829,11 @@ export class LinearElementEditor { const targetPoint = clickedPointIndex > -1 && pointRotateRads( - point( + pointFrom( element.x + element.points[clickedPointIndex][0], element.y + element.points[clickedPointIndex][1], ), - point(cx, cy), + pointFrom(cx, cy), element.angle, ); @@ -926,11 +928,11 @@ export class LinearElementEditor { element, elementsMap, lastCommittedPoint, - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); - newPoint = point( + newPoint = pointFrom( width + lastCommittedPoint[0], height + lastCommittedPoint[1], ); @@ -982,8 +984,8 @@ export class LinearElementEditor { const { x, y } = element; return pointRotateRads( - point(x + p[0], y + p[1]), - point(cx, cy), + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), element.angle, ); } @@ -999,8 +1001,8 @@ export class LinearElementEditor { return element.points.map((p) => { const { x, y } = element; return pointRotateRads( - point(x + p[0], y + p[1]), - point(cx, cy), + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), element.angle, ); }); @@ -1023,8 +1025,12 @@ export class LinearElementEditor { const { x, y } = element; return p - ? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle) - : pointRotateRads(point(x, y), point(cx, cy), element.angle); + ? pointRotateRads( + pointFrom(x + p[0], y + p[1]), + pointFrom(cx, cy), + element.angle, + ) + : pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle); } static pointFromAbsoluteCoords( @@ -1034,7 +1040,7 @@ export class LinearElementEditor { ): LocalPoint { if (isElbowArrow(element)) { // No rotation for elbow arrows - return point( + return pointFrom( absoluteCoords[0] - element.x, absoluteCoords[1] - element.y, ); @@ -1044,11 +1050,11 @@ export class LinearElementEditor { const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [x, y] = pointRotateRads( - point(absoluteCoords[0], absoluteCoords[1]), - point(cx, cy), + pointFrom(absoluteCoords[0], absoluteCoords[1]), + pointFrom(cx, cy), -element.angle as Radians, ); - return point(x - element.x, y - element.y); + return pointFrom(x - element.x, y - element.y); } static getPointIndexUnderCursor( @@ -1069,7 +1075,7 @@ export class LinearElementEditor { while (--idx > -1) { const p = pointHandles[idx]; if ( - pointDistance(point(x, y), point(p[0], p[1])) * zoom.value < + pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value < // +1px to account for outline stroke LinearElementEditor.POINT_HANDLE_SIZE + 1 ) { @@ -1091,12 +1097,12 @@ export class LinearElementEditor { const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const [rotatedX, rotatedY] = pointRotateRads( - point(pointerOnGrid[0], pointerOnGrid[1]), - point(cx, cy), + pointFrom(pointerOnGrid[0], pointerOnGrid[1]), + pointFrom(cx, cy), -element.angle as Radians, ); - return point(rotatedX - element.x, rotatedY - element.y); + return pointFrom(rotatedX - element.x, rotatedY - element.y); } /** @@ -1116,7 +1122,7 @@ export class LinearElementEditor { return { points: points.map((p) => { - return point(p[0] - offsetX, p[1] - offsetY); + return pointFrom(p[0] - offsetX, p[1] - offsetY); }), x: element.x + offsetX, y: element.y + offsetY, @@ -1170,8 +1176,8 @@ export class LinearElementEditor { } acc.push( nextPoint - ? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) - : point(p[0], p[1]), + ? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) + : pointFrom(p[0], p[1]), ); nextSelectedIndices.push(indexCursor + 1); @@ -1192,7 +1198,7 @@ export class LinearElementEditor { [ { index: element.points.length - 1, - point: point(lastPoint[0] + 30, lastPoint[1] + 30), + point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30), }, ], elementsMap, @@ -1233,7 +1239,9 @@ export class LinearElementEditor { const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => { if (!pointIndices.includes(idx)) { acc.push( - !acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY), + !acc.length + ? pointFrom(0, 0) + : pointFrom(p[0] - offsetX, p[1] - offsetY), ); } return acc; @@ -1310,9 +1318,9 @@ export class LinearElementEditor { const deltaY = selectedPointData.point[1] - points[selectedPointData.index][1]; - return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); + return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); } - return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p; + return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p; }); LinearElementEditor._updatePoints( @@ -1366,8 +1374,8 @@ export class LinearElementEditor { const origin = linearElementEditor.pointerDownState.origin!; const dist = pointDistance( - point(origin.x, origin.y), - point(pointerCoords.x, pointerCoords.y), + pointFrom(origin.x, origin.y), + pointFrom(pointerCoords.x, pointerCoords.y), ); if ( !appState.editingLinearElement && @@ -1477,7 +1485,9 @@ export class LinearElementEditor { nextPoints, vector(offsetX, offsetY), bindings, - options, + { + isDragging: options?.isDragging, + }, ); } else { const nextCoords = getElementPointsCoords(element, nextPoints); @@ -1489,8 +1499,8 @@ export class LinearElementEditor { const dX = prevCenterX - nextCenterX; const dY = prevCenterY - nextCenterY; const rotated = pointRotateRads( - point(offsetX, offsetY), - point(dX, dY), + pointFrom(offsetX, offsetY), + pointFrom(dX, dY), element.angle, ); mutateElement(element, { @@ -1536,8 +1546,8 @@ export class LinearElementEditor { ); return pointRotateRads( - point(width, height), - point(0, 0), + pointFrom(width, height), + pointFrom(0, 0), -element.angle as Radians, ); } @@ -1607,36 +1617,36 @@ export class LinearElementEditor { ); const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextY2 = boundTextY1 + boundTextElement.height; - const centerPoint = point(cx, cy); + const centerPoint = pointFrom(cx, cy); const topLeftRotatedPoint = pointRotateRads( - point(x1, y1), + pointFrom(x1, y1), centerPoint, element.angle, ); const topRightRotatedPoint = pointRotateRads( - point(x2, y1), + pointFrom(x2, y1), centerPoint, element.angle, ); const counterRotateBoundTextTopLeft = pointRotateRads( - point(boundTextX1, boundTextY1), + pointFrom(boundTextX1, boundTextY1), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextTopRight = pointRotateRads( - point(boundTextX2, boundTextY1), + pointFrom(boundTextX2, boundTextY1), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextBottomLeft = pointRotateRads( - point(boundTextX1, boundTextY2), + pointFrom(boundTextX1, boundTextY2), centerPoint, -element.angle as Radians, ); const counterRotateBoundTextBottomRight = pointRotateRads( - point(boundTextX2, boundTextY2), + pointFrom(boundTextX2, boundTextY2), centerPoint, -element.angle as Radians, ); diff --git a/packages/excalidraw/element/newElement.test.ts b/packages/excalidraw/element/newElement.test.ts index 770d0d987..6a74e58f0 100644 --- a/packages/excalidraw/element/newElement.test.ts +++ b/packages/excalidraw/element/newElement.test.ts @@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants"; import { isPrimitive } from "../utils"; import type { ExcalidrawLinearElement } from "./types"; import type { LocalPoint } from "../../math"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const assertCloneObjects = (source: any, clone: any) => { for (const key in clone) { @@ -38,7 +38,7 @@ describe("duplicating single elements", () => { element.__proto__ = { hello: "world" }; mutateElement(element, { - points: [point (1, 2), point (3, 4)], + points: [pointFrom (1, 2), pointFrom (3, 4)], }); const copy = duplicateElement(null, new Map(), element); diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index e21d36617..55aa011f7 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -223,7 +223,6 @@ export const newTextElement = ( verticalAlign?: VerticalAlign; containerId?: ExcalidrawTextContainer["id"] | null; lineHeight?: ExcalidrawTextElement["lineHeight"]; - strokeWidth?: ExcalidrawTextElement["strokeWidth"]; autoResize?: ExcalidrawTextElement["autoResize"]; } & ElementConstructorOpts, ): NonDeleted => { diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 3f3f8ef1e..08ca5543f 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -9,6 +9,7 @@ import type { ExcalidrawTextElementWithContainer, ExcalidrawImageElement, ElementsMap, + ExcalidrawArrowElement, NonDeletedSceneElementsMap, SceneElementsMap, } from "./types"; @@ -57,7 +58,7 @@ import type { GlobalPoint } from "../../math"; import { pointCenter, normalizeRadians, - point, + pointFrom, pointFromPair, pointRotateRads, type Radians, @@ -239,8 +240,8 @@ const resizeSingleTextElement = ( ); // rotation pointer with reverse angle const [rotatedX, rotatedY] = pointRotateRads( - point(pointerX, pointerY), - point(cx, cy), + pointFrom(pointerX, pointerY), + pointFrom(cx, cy), -element.angle as Radians, ); let scaleX = 0; @@ -275,23 +276,23 @@ const resizeSingleTextElement = ( const startBottomRight = [x2, y2]; const startCenter = [cx, cy]; - let newTopLeft = point (x1, y1); + let newTopLeft = pointFrom (x1, y1); if (["n", "w", "nw"].includes(transformHandleType)) { - newTopLeft = point ( + newTopLeft = pointFrom ( startBottomRight[0] - Math.abs(nextWidth), startBottomRight[1] - Math.abs(nextHeight), ); } if (transformHandleType === "ne") { const bottomLeft = [startTopLeft[0], startBottomRight[1]]; - newTopLeft = point ( + newTopLeft = pointFrom ( bottomLeft[0], bottomLeft[1] - Math.abs(nextHeight), ); } if (transformHandleType === "sw") { const topRight = [startBottomRight[0], startTopLeft[1]]; - newTopLeft = point ( + newTopLeft = pointFrom ( topRight[0] - Math.abs(nextWidth), topRight[1], ); @@ -310,12 +311,20 @@ const resizeSingleTextElement = ( } const angle = element.angle; - const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle); - const newCenter = point ( + const rotatedTopLeft = pointRotateRads( + newTopLeft, + pointFrom(cx, cy), + angle, + ); + const newCenter = pointFrom ( newTopLeft[0] + Math.abs(nextWidth) / 2, newTopLeft[1] + Math.abs(nextHeight) / 2, ); - const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle); + const rotatedNewCenter = pointRotateRads( + newCenter, + pointFrom(cx, cy), + angle, + ); newTopLeft = pointRotateRads( rotatedTopLeft, rotatedNewCenter, @@ -340,12 +349,12 @@ const resizeSingleTextElement = ( stateAtResizeStart.height, true, ); - const startTopLeft = point (x1, y1); - const startBottomRight = point (x2, y2); + const startTopLeft = pointFrom (x1, y1); + const startBottomRight = pointFrom (x2, y2); const startCenter = pointCenter(startTopLeft, startBottomRight); const rotatedPointer = pointRotateRads( - point(pointerX, pointerY), + pointFrom(pointerX, pointerY), startCenter, -stateAtResizeStart.angle as Radians, ); @@ -418,7 +427,7 @@ const resizeSingleTextElement = ( startCenter, angle, ); - const newCenter = point( + const newCenter = pointFrom( newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2, ); @@ -460,13 +469,13 @@ export const resizeSingleElement = ( stateAtResizeStart.height, true, ); - const startTopLeft = point(x1, y1); - const startBottomRight = point(x2, y2); + const startTopLeft = pointFrom(x1, y1); + const startBottomRight = pointFrom(x2, y2); const startCenter = pointCenter(startTopLeft, startBottomRight); // Calculate new dimensions based on cursor position const rotatedPointer = pointRotateRads( - point(pointerX, pointerY), + pointFrom(pointerX, pointerY), startCenter, -stateAtResizeStart.angle as Radians, ); @@ -647,7 +656,7 @@ export const resizeSingleElement = ( startCenter, angle, ); - const newCenter = point( + const newCenter = pointFrom( newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2, ); @@ -816,20 +825,20 @@ export const resizeMultipleElements = ( const direction = transformHandleType; const anchorsMap: Record = { - ne: point(minX, maxY), - se: point(minX, minY), - sw: point(maxX, minY), - nw: point(maxX, maxY), - e: point(minX, minY + height / 2), - w: point(maxX, minY + height / 2), - n: point(minX + width / 2, maxY), - s: point(minX + width / 2, minY), + ne: pointFrom(minX, maxY), + se: pointFrom(minX, minY), + sw: pointFrom(maxX, minY), + nw: pointFrom(maxX, maxY), + e: pointFrom(minX, minY + height / 2), + w: pointFrom(maxX, minY + height / 2), + n: pointFrom(minX + width / 2, maxY), + s: pointFrom(minX + width / 2, minY), }; // anchor point must be on the opposite side of the dragged selection handle // or be the center of the selection if shouldResizeFromCenter const [anchorX, anchorY] = shouldResizeFromCenter - ? point(midX, midY) + ? pointFrom(midX, midY) : anchorsMap[direction]; const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1; @@ -909,6 +918,8 @@ export const resizeMultipleElements = ( fontSize?: ExcalidrawTextElement["fontSize"]; scale?: ExcalidrawImageElement["scale"]; boundTextFontSize?: ExcalidrawTextElement["fontSize"]; + startBinding?: ExcalidrawArrowElement["startBinding"]; + endBinding?: ExcalidrawArrowElement["endBinding"]; }; }[] = []; @@ -993,19 +1004,6 @@ export const resizeMultipleElements = ( mutateElement(element, update, false); - if (isArrowElement(element) && isElbowArrow(element)) { - mutateElbowArrow( - element, - elementsMap, - element.points, - undefined, - undefined, - { - informMutation: false, - }, - ); - } - updateBoundElements(element, elementsMap, { simultaneouslyUpdated: elementsToUpdate, oldSize: { width: oldWidth, height: oldHeight }, @@ -1054,12 +1052,12 @@ const rotateMultipleElements = ( const origAngle = originalElements.get(element.id)?.angle ?? element.angle; const [rotatedCX, rotatedCY] = pointRotateRads( - point(cx, cy), - point(centerX, centerY), + pointFrom(cx, cy), + pointFrom(centerX, centerY), (centerAngle + origAngle - element.angle) as Radians, ); - if (isArrowElement(element) && isElbowArrow(element)) { + if (isElbowArrow(element)) { const points = getArrowLocalFixedPoints(element, elementsMap); mutateElbowArrow(element, elementsMap, points); } else { @@ -1111,40 +1109,44 @@ export const getResizeOffsetXY = ( const angle = ( selectedElements.length === 1 ? selectedElements[0].angle : 0 ) as Radians; - [x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians); + [x, y] = pointRotateRads( + pointFrom(x, y), + pointFrom(cx, cy), + -angle as Radians, + ); switch (transformHandleType) { case "n": return pointRotateRads( - point(x - (x1 + x2) / 2, y - y1), - point(0, 0), + pointFrom(x - (x1 + x2) / 2, y - y1), + pointFrom(0, 0), angle, ); case "s": return pointRotateRads( - point(x - (x1 + x2) / 2, y - y2), - point(0, 0), + pointFrom(x - (x1 + x2) / 2, y - y2), + pointFrom(0, 0), angle, ); case "w": return pointRotateRads( - point(x - x1, y - (y1 + y2) / 2), - point(0, 0), + pointFrom(x - x1, y - (y1 + y2) / 2), + pointFrom(0, 0), angle, ); case "e": return pointRotateRads( - point(x - x2, y - (y1 + y2) / 2), - point(0, 0), + pointFrom(x - x2, y - (y1 + y2) / 2), + pointFrom(0, 0), angle, ); case "nw": - return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle); case "ne": - return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle); case "sw": - return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle); case "se": - return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle); + return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle); default: return [0, 0]; } diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts index c363f6180..5fcae5335 100644 --- a/packages/excalidraw/element/resizeTest.ts +++ b/packages/excalidraw/element/resizeTest.ts @@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants"; import { isLinearElement } from "./typeChecks"; import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import { - point, + pointFrom, pointOnLineSegment, pointRotateRads, type Radians, @@ -92,16 +92,20 @@ export const resizeTest = ( if (!(isLinearElement(element) && element.points.length <= 2)) { const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const sides = getSelectionBorders( - point(x1 - SPACING, y1 - SPACING), - point(x2 + SPACING, y2 + SPACING), - point(cx, cy), + pointFrom(x1 - SPACING, y1 - SPACING), + pointFrom(x2 + SPACING, y2 + SPACING), + pointFrom(cx, cy), element.angle, ); for (const [dir, side] of Object.entries(sides)) { // test to see if x, y are on the line segment if ( - pointOnLineSegment(point(x, y), side as LineSegment , SPACING) + pointOnLineSegment( + pointFrom(x, y), + side as LineSegment , + SPACING, + ) ) { return dir as TransformHandleType; } @@ -178,9 +182,9 @@ export const getTransformHandleTypeFromCoords = < const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const sides = getSelectionBorders( - point(x1 - SPACING, y1 - SPACING), - point(x2 + SPACING, y2 + SPACING), - point(cx, cy), + pointFrom(x1 - SPACING, y1 - SPACING), + pointFrom(x2 + SPACING, y2 + SPACING), + pointFrom(cx, cy), 0 as Radians, ); @@ -188,7 +192,7 @@ export const getTransformHandleTypeFromCoords = < // test to see if x, y are on the line segment if ( pointOnLineSegment( - point(scenePointerX, scenePointerY), + pointFrom(scenePointerX, scenePointerY), side as LineSegment , SPACING, ) @@ -265,10 +269,10 @@ const getSelectionBorders = ( center: Point, angle: Radians, ) => { - const topLeft = pointRotateRads(point(x1, y1), center, angle); - const topRight = pointRotateRads(point(x2, y1), center, angle); - const bottomLeft = pointRotateRads(point(x1, y2), center, angle); - const bottomRight = pointRotateRads(point(x2, y2), center, angle); + const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle); + const topRight = pointRotateRads(pointFrom(x2, y1), center, angle); + const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle); + const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle); return { n: [topLeft, topRight], diff --git a/packages/excalidraw/element/routing.test.tsx b/packages/excalidraw/element/routing.test.tsx index 9381541a5..fb6b23f28 100644 --- a/packages/excalidraw/element/routing.test.tsx +++ b/packages/excalidraw/element/routing.test.tsx @@ -17,7 +17,7 @@ import type { ExcalidrawElbowArrowElement, } from "./types"; import { ARROW_TYPE } from "../constants"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; const { h } = window; @@ -32,8 +32,8 @@ describe("elbow arrow routing", () => { }) as ExcalidrawElbowArrowElement; scene.insertElement(arrow); mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [ - point(-45 - arrow.x, -100.1 - arrow.y), - point(45 - arrow.x, 99.9 - arrow.y), + pointFrom(-45 - arrow.x, -100.1 - arrow.y), + pointFrom(45 - arrow.x, 99.9 - arrow.y), ]); expect(arrow.points).toEqual([ [0, 0], @@ -69,7 +69,7 @@ describe("elbow arrow routing", () => { y: -100.1, width: 90, height: 200, - points: [point(0, 0), point(90, 200)], + points: [pointFrom(0, 0), pointFrom(90, 200)], }) as ExcalidrawElbowArrowElement; scene.insertElement(rectangle1); scene.insertElement(rectangle2); @@ -81,7 +81,7 @@ describe("elbow arrow routing", () => { expect(arrow.startBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null); - mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]); + mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]); expect(arrow.points).toEqual([ [0, 0], @@ -94,7 +94,16 @@ describe("elbow arrow routing", () => { describe("elbow arrow ui", () => { beforeEach(async () => { + localStorage.clear(); await render( ); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByTestId(contextMenu!, "stats")!); }); it("can follow bound shapes", async () => { @@ -130,8 +139,8 @@ describe("elbow arrow ui", () => { expect(arrow.elbowed).toBe(true); expect(arrow.points).toEqual([ [0, 0], - [35, 0], - [35, 200], + [45, 0], + [45, 200], [90, 200], ]); }); @@ -163,14 +172,6 @@ describe("elbow arrow ui", () => { h.state, )[0] as ExcalidrawArrowElement; - fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { - button: 2, - clientX: 1, - clientY: 1, - }); - const contextMenu = UI.queryContextMenu(); - fireEvent.click(queryByTestId(contextMenu!, "stats")!); - mouse.click(51, 51); const inputAngle = UI.queryStatsProperty("A")?.querySelector( @@ -182,8 +183,8 @@ describe("elbow arrow ui", () => { [0, 0], [35, 0], [35, 90], - [25, 90], - [25, 165], + [35, 90], // Note that coordinates are rounded above! + [35, 165], [103, 165], ]); }); diff --git a/packages/excalidraw/element/routing.ts b/packages/excalidraw/element/routing.ts index 07f62ca82..c8b1c2d43 100644 --- a/packages/excalidraw/element/routing.ts +++ b/packages/excalidraw/element/routing.ts @@ -1,6 +1,6 @@ import type { Radians } from "../../math"; import { - point, + pointFrom, pointScaleFromOrigin, pointTranslate, vector, @@ -36,11 +36,11 @@ import { HEADING_UP, vectorToHeading, } from "./heading"; +import type { ElementUpdate } from "./mutateElement"; import { mutateElement } from "./mutateElement"; import { isBindableElement, isRectanguloidElement } from "./typeChecks"; import type { ExcalidrawElbowArrowElement, - FixedPointBinding, NonDeletedSceneElementsMap, SceneElementsMap, } from "./types"; @@ -72,16 +72,48 @@ export const mutateElbowArrow = ( elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, nextPoints: readonly LocalPoint[], offset?: Vector, - otherUpdates?: { - startBinding?: FixedPointBinding | null; - endBinding?: FixedPointBinding | null; + otherUpdates?: Omit< + ElementUpdate , + "angle" | "x" | "y" | "width" | "height" | "elbowed" | "points" + >, + options?: { + isDragging?: boolean; + informMutation?: boolean; }, +) => { + const update = updateElbowArrow( + arrow, + elementsMap, + nextPoints, + offset, + options, + ); + if (update) { + mutateElement( + arrow, + { + ...otherUpdates, + ...update, + angle: 0 as Radians, + }, + options?.informMutation, + ); + } else { + console.error("Elbow arrow cannot find a route"); + } +}; + +export const updateElbowArrow = ( + arrow: ExcalidrawElbowArrowElement, + elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, + nextPoints: readonly LocalPoint[], + offset?: Vector, options?: { isDragging?: boolean; disableBinding?: boolean; informMutation?: boolean; }, -) => { +): ElementUpdate | null => { const origStartGlobalPoint: GlobalPoint = pointTranslate( pointTranslate ( nextPoints[0], @@ -235,6 +267,8 @@ export const mutateElbowArrow = ( BASE_PADDING, ), boundsOverlap, + hoveredStartElement && aabbForElement(hoveredStartElement), + hoveredEndElement && aabbForElement(hoveredEndElement), ); const startDonglePosition = getDonglePosition( dynamicAABBs[0], @@ -295,18 +329,10 @@ export const mutateElbowArrow = ( startDongle && points.unshift(startGlobalPoint); endDongle && points.push(endGlobalPoint); - mutateElement( - arrow, - { - ...otherUpdates, - ...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0), - angle: 0 as Radians, - }, - options?.informMutation, - ); - } else { - console.error("Elbow arrow cannot find a route"); + return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0); } + + return null; }; const offsetFromHeading = ( @@ -475,7 +501,11 @@ const generateDynamicAABBs = ( startDifference?: [number, number, number, number], endDifference?: [number, number, number, number], disableSideHack?: boolean, + startElementBounds?: Bounds | null, + endElementBounds?: Bounds | null, ): Bounds[] => { + const startEl = startElementBounds ?? a; + const endEl = endElementBounds ?? b; const [startUp, startRight, startDown, startLeft] = startDifference ?? [ 0, 0, 0, 0, ]; @@ -484,29 +514,29 @@ const generateDynamicAABBs = ( const first = [ a[0] > b[2] ? a[1] > b[3] || a[3] < b[1] - ? Math.min((a[0] + b[2]) / 2, a[0] - startLeft) - : (a[0] + b[2]) / 2 + ? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft) + : (startEl[0] + endEl[2]) / 2 : a[0] > b[0] ? a[0] - startLeft : common[0] - startLeft, a[1] > b[3] ? a[0] > b[2] || a[2] < b[0] - ? Math.min((a[1] + b[3]) / 2, a[1] - startUp) - : (a[1] + b[3]) / 2 + ? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp) + : (startEl[1] + endEl[3]) / 2 : a[1] > b[1] ? a[1] - startUp : common[1] - startUp, a[2] < b[0] ? a[1] > b[3] || a[3] < b[1] - ? Math.max((a[2] + b[0]) / 2, a[2] + startRight) - : (a[2] + b[0]) / 2 + ? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight) + : (startEl[2] + endEl[0]) / 2 : a[2] < b[2] ? a[2] + startRight : common[2] + startRight, a[3] < b[1] ? a[0] > b[2] || a[2] < b[0] - ? Math.max((a[3] + b[1]) / 2, a[3] + startDown) - : (a[3] + b[1]) / 2 + ? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown) + : (startEl[3] + endEl[1]) / 2 : a[3] < b[3] ? a[3] + startDown : common[3] + startDown, @@ -514,29 +544,29 @@ const generateDynamicAABBs = ( const second = [ b[0] > a[2] ? b[1] > a[3] || b[3] < a[1] - ? Math.min((b[0] + a[2]) / 2, b[0] - endLeft) - : (b[0] + a[2]) / 2 + ? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft) + : (endEl[0] + startEl[2]) / 2 : b[0] > a[0] ? b[0] - endLeft : common[0] - endLeft, b[1] > a[3] ? b[0] > a[2] || b[2] < a[0] - ? Math.min((b[1] + a[3]) / 2, b[1] - endUp) - : (b[1] + a[3]) / 2 + ? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp) + : (endEl[1] + startEl[3]) / 2 : b[1] > a[1] ? b[1] - endUp : common[1] - endUp, b[2] < a[0] ? b[1] > a[3] || b[3] < a[1] - ? Math.max((b[2] + a[0]) / 2, b[2] + endRight) - : (b[2] + a[0]) / 2 + ? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight) + : (endEl[2] + startEl[0]) / 2 : b[2] < a[2] ? b[2] + endRight : common[2] + endRight, b[3] < a[1] ? b[0] > a[2] || b[2] < a[0] - ? Math.max((b[3] + a[1]) / 2, b[3] + endDown) - : (b[3] + a[1]) / 2 + ? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown) + : (endEl[3] + startEl[1]) / 2 : b[3] < a[3] ? b[3] + endDown : common[3] + endDown, @@ -713,13 +743,13 @@ const getDonglePosition = ( ): GlobalPoint => { switch (heading) { case HEADING_UP: - return point(p[0], bounds[1]); + return pointFrom(p[0], bounds[1]); case HEADING_RIGHT: - return point(bounds[2], p[1]); + return pointFrom(bounds[2], p[1]); case HEADING_DOWN: - return point(p[0], bounds[3]); + return pointFrom(p[0], bounds[3]); } - return point(bounds[0], p[1]); + return pointFrom(bounds[0], p[1]); }; const estimateSegmentCount = ( diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts index b10f31f32..f633789a9 100644 --- a/packages/excalidraw/element/sizeHelpers.ts +++ b/packages/excalidraw/element/sizeHelpers.ts @@ -2,7 +2,7 @@ import type { ElementsMap, ExcalidrawElement } from "./types"; import { mutateElement } from "./mutateElement"; import { isFreeDrawElement, isLinearElement } from "./typeChecks"; import { SHIFT_LOCKING_ANGLE } from "../constants"; -import type { AppState, Zoom } from "../types"; +import type { AppState, Offsets, Zoom } from "../types"; import { getCommonBounds, getElementBounds } from "./bounds"; import { viewportCoordsToSceneCoords } from "../utils"; @@ -67,12 +67,7 @@ export const isElementCompletelyInViewport = ( scrollY: number; }, elementsMap: ElementsMap, - padding?: Partial<{ - top: number; - right: number; - bottom: number; - left: number; - }>, + padding?: Offsets, ) => { const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates const topLeftSceneCoords = viewportCoordsToSceneCoords( diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index 9fb4766b6..9abebc356 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -284,16 +284,17 @@ export const measureText = ( text: string, font: FontString, lineHeight: ExcalidrawTextElement["lineHeight"], + forceAdvanceWidth?: true, ) => { - text = text + const _text = text .split("\n") // replace empty lines with single space because leading/trailing empty // lines would be stripped from computation .map((x) => x || " ") .join("\n"); const fontSize = parseFloat(font); - const height = getTextHeight(text, fontSize, lineHeight); - const width = getTextWidth(text, font); + const height = getTextHeight(_text, fontSize, lineHeight); + const width = getTextWidth(_text, font, forceAdvanceWidth); return { width, height }; }; diff --git a/packages/excalidraw/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx index 98063f05b..ea57ca190 100644 --- a/packages/excalidraw/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -19,7 +19,7 @@ import type { import { API } from "../tests/helpers/api"; import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; -import { point } from "../../math"; +import { pointFrom } from "../../math"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -42,7 +42,7 @@ describe("textWysiwyg", () => { type: "line", width: 100, height: 0, - points: [point(0, 0), point(100, 0)], + points: [pointFrom(0, 0), pointFrom(100, 0)], }); const textSize = 20; const text = API.createElement({ diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 2281a0cc3..23778cb7b 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -247,7 +247,7 @@ export const textWysiwyg = ({ // adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari) const padding = !isSafari - ? Math.ceil(updatedTextElement.fontSize / 2) + ? Math.ceil(updatedTextElement.fontSize / appState.zoom.value / 2) : 0; // Make sure text editor height doesn't go beyond viewport diff --git a/packages/excalidraw/element/transformHandles.ts b/packages/excalidraw/element/transformHandles.ts index 173c9fdc9..ccd68b282 100644 --- a/packages/excalidraw/element/transformHandles.ts +++ b/packages/excalidraw/element/transformHandles.ts @@ -19,7 +19,7 @@ import { isIOS, } from "../constants"; import type { Radians } from "../../math"; -import { point, pointRotateRads } from "../../math"; +import { pointFrom, pointRotateRads } from "../../math"; export type TransformHandleDirection = | "n" @@ -95,8 +95,8 @@ const generateTransformHandle = ( angle: Radians, ): TransformHandle => { const [xx, yy] = pointRotateRads( - point(x + width / 2, y + height / 2), - point(cx, cy), + pointFrom(x + width / 2, y + height / 2), + pointFrom(cx, cy), angle, ); return [xx - width / 2, yy - height / 2, width, height]; diff --git a/packages/excalidraw/element/typeChecks.ts b/packages/excalidraw/element/typeChecks.ts index 5ba089ab0..6bb4269f8 100644 --- a/packages/excalidraw/element/typeChecks.ts +++ b/packages/excalidraw/element/typeChecks.ts @@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = ( }; export const isFixedPointBinding = ( - binding: PointBinding, + binding: PointBinding | FixedPointBinding, ): binding is FixedPointBinding => { - return binding.fixedPoint != null; + return ( + Object.hasOwn(binding, "fixedPoint") && + (binding as FixedPointBinding).fixedPoint != null + ); }; // TODO: Move this to @excalidraw/math diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 98624875b..c804d8525 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -202,6 +202,7 @@ export type ExcalidrawElement = | ExcalidrawGenericElement | ExcalidrawTextElement | ExcalidrawLinearElement + | ExcalidrawArrowElement | ExcalidrawFreeDrawElement | ExcalidrawImageElement | ExcalidrawFrameElement @@ -277,15 +278,19 @@ export type PointBinding = { elementId: ExcalidrawBindableElement["id"]; focus: number; gap: number; - // Represents the fixed point binding information in form of a vertical and - // horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio - // gives the user selected fixed point by multiplying the bound element width - // with fixedPoint[0] and the bound element height with fixedPoint[1] to get the - // bound element-local point coordinate. - fixedPoint: FixedPoint | null; }; -export type FixedPointBinding = Merge ; +export type FixedPointBinding = Merge< + PointBinding, + { + // Represents the fixed point binding information in form of a vertical and + // horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio + // gives the user selected fixed point by multiplying the bound element width + // with fixedPoint[0] and the bound element height with fixedPoint[1] to get the + // bound element-local point coordinate. + fixedPoint: FixedPoint; + } +>; export type Arrowhead = | "arrow" diff --git a/packages/excalidraw/fonts/ExcalidrawFont.ts b/packages/excalidraw/fonts/ExcalidrawFont.ts index 682ae7394..51d6578c6 100644 --- a/packages/excalidraw/fonts/ExcalidrawFont.ts +++ b/packages/excalidraw/fonts/ExcalidrawFont.ts @@ -1,4 +1,8 @@ -import { stringToBase64, toByteString } from "../data/encode"; +import { + base64ToArrayBuffer, + stringToBase64, + toByteString, +} from "../data/encode"; import { LOCAL_FONT_PROTOCOL } from "./metadata"; import loadWoff2 from "./wasm/woff2.loader"; import loadHbSubset from "./wasm/hb-subset.loader"; @@ -49,10 +53,7 @@ export class ExcalidrawFont implements Font { // it's dataurl (server), the font is inlined as base64, no need to fetch if (url.protocol === "data:") { - const arrayBuffer = Buffer.from( - url.toString().split(",")[1], - "base64", - ).buffer; + const arrayBuffer = base64ToArrayBuffer(url.toString().split(",")[1]); const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints( arrayBuffer, diff --git a/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 b/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 new file mode 100644 index 000000000..51e6f53a8 Binary files /dev/null and b/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 b/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 new file mode 100644 index 000000000..1fe1443ba Binary files /dev/null and b/packages/excalidraw/fonts/assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 new file mode 100644 index 000000000..b9b1c20c0 Binary files /dev/null and b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 new file mode 100644 index 000000000..c9f8bb017 Binary files /dev/null and b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 new file mode 100644 index 000000000..6b0697140 Binary files /dev/null and b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 new file mode 100644 index 000000000..c21a6ed08 Binary files /dev/null and b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 differ diff --git a/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 new file mode 100644 index 000000000..bd56dc50d Binary files /dev/null and b/packages/excalidraw/fonts/assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 differ diff --git a/packages/excalidraw/fonts/index.ts b/packages/excalidraw/fonts/index.ts index 39f6bf8da..1de1f99c9 100644 --- a/packages/excalidraw/fonts/index.ts +++ b/packages/excalidraw/fonts/index.ts @@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2"; import ComicShanns from "./assets/ComicShanns-Regular.woff2"; import LiberationSans from "./assets/LiberationSans-Regular.woff2"; -import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; -import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; +import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; +import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; -import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; -import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; -import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; -import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; -import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; +import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; +import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; +import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; +import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; +import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; export class Fonts { // it's ok to track fonts across multiple instances only once, so let's use diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index fb9a45820..a8e91265f 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -29,7 +29,7 @@ import { getElementLineSegments } from "./element/bounds"; import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; import type { ReadonlySetLike } from "./utility-types"; -import { isPointWithinBounds, point } from "../math"; +import { isPointWithinBounds, pointFrom } from "../math"; // --------------------------- Frame State ------------------------------------ export const bindElementsToFramesAfterDuplication = ( @@ -159,9 +159,9 @@ export const isCursorInFrame = ( const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); return isPointWithinBounds( - point(fx1, fy1), - point(cursorCoords.x, cursorCoords.y), - point(fx2, fy2), + pointFrom(fx1, fy1), + pointFrom(cursorCoords.x, cursorCoords.y), + pointFrom(fx2, fy2), ); }; diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index ebf9ff872..e4c5eea44 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -162,6 +162,13 @@ "hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.", "hint_emptyPrivateLibrary": "Select an item on canvas to add it here." }, + "search": { + "title": "Find on canvas", + "noMatch": "No matches found...", + "singleResult": "result", + "multipleResults": "results", + "placeholder": "Find text on canvas..." + }, "buttons": { "clearReset": "Reset the canvas", "exportJSON": "Export to file", @@ -297,6 +304,7 @@ "shapes": "Shapes" }, "hints": { + "dismissSearch": "Escape to dismiss search", "canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool", "linearElement": "Click to start multiple points, drag for single line", "arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.", diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index db349559b..243d5d45a 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -30,8 +30,12 @@ import { shouldShowBoundingBox, } from "../element/transformHandles"; import { arrayToMap, throttleRAF } from "../utils"; -import type { InteractiveCanvasAppState } from "../types"; -import { DEFAULT_TRANSFORM_HANDLE_SPACING, FRAME_STYLE } from "../constants"; +import { + DEFAULT_TRANSFORM_HANDLE_SPACING, + FRAME_STYLE, + THEME, +} from "../constants"; +import { type InteractiveCanvasAppState } from "../types"; import { renderSnaps } from "../renderer/renderSnaps"; @@ -48,7 +52,6 @@ import { } from "./helpers"; import oc from "open-color"; import { - isArrowElement, isElbowArrow, isFrameLikeElement, isLinearElement, @@ -901,7 +904,6 @@ const _renderInteractiveScene = ({ // Elbow arrow elements cannot be selected when bound on either end ( isSingleLinearElementSelected && - isArrowElement(element) && isElbowArrow(element) && (element.startBinding || element.endBinding) ) @@ -1066,9 +1068,48 @@ const _renderInteractiveScene = ({ context.restore(); } + appState.searchMatches.forEach(({ id, focus, matchedLines }) => { + const element = elementsMap.get(id); + + if (element && isTextElement(element)) { + const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + true, + ); + + context.save(); + if (appState.theme === THEME.LIGHT) { + if (focus) { + context.fillStyle = "rgba(255, 124, 0, 0.4)"; + } else { + context.fillStyle = "rgba(255, 226, 0, 0.4)"; + } + } else if (focus) { + context.fillStyle = "rgba(229, 82, 0, 0.4)"; + } else { + context.fillStyle = "rgba(99, 52, 0, 0.4)"; + } + + context.translate(appState.scrollX, appState.scrollY); + context.translate(cx, cy); + context.rotate(element.angle); + + matchedLines.forEach((matchedLine) => { + context.fillRect( + elementX1 + matchedLine.offsetX - cx, + elementY1 + matchedLine.offsetY - cy, + matchedLine.width, + matchedLine.height, + ); + }); + + context.restore(); + } + }); + renderSnaps(context, appState); - // Reset zoom context.restore(); renderRemoteCursors({ diff --git a/packages/excalidraw/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts index 33b57ce68..57b57c570 100644 --- a/packages/excalidraw/renderer/renderSnaps.ts +++ b/packages/excalidraw/renderer/renderSnaps.ts @@ -1,4 +1,4 @@ -import { point, type GlobalPoint, type LocalPoint } from "../../math"; +import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math"; import { THEME } from "../constants"; import type { PointSnapLine, PointerSnapLine } from "../snapping"; import type { InteractiveCanvasAppState } from "../types"; @@ -140,27 +140,31 @@ const drawGapLine = ( // (1) if (!appState.zenModeEnabled) { drawLine( - point(from[0], from[1] - FULL), - point(from[0], from[1] + FULL), + pointFrom(from[0], from[1] - FULL), + pointFrom(from[0], from[1] + FULL), context, ); } // (3) drawLine( - point(halfPoint[0] - QUARTER, halfPoint[1] - HALF), - point(halfPoint[0] - QUARTER, halfPoint[1] + HALF), + pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF), + pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF), context, ); drawLine( - point(halfPoint[0] + QUARTER, halfPoint[1] - HALF), - point(halfPoint[0] + QUARTER, halfPoint[1] + HALF), + pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF), + pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF), context, ); if (!appState.zenModeEnabled) { // (4) - drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context); + drawLine( + pointFrom(to[0], to[1] - FULL), + pointFrom(to[0], to[1] + FULL), + context, + ); // (2) drawLine(from, to, context); @@ -170,27 +174,31 @@ const drawGapLine = ( // (1) if (!appState.zenModeEnabled) { drawLine( - point(from[0] - FULL, from[1]), - point(from[0] + FULL, from[1]), + pointFrom(from[0] - FULL, from[1]), + pointFrom(from[0] + FULL, from[1]), context, ); } // (3) drawLine( - point(halfPoint[0] - HALF, halfPoint[1] - QUARTER), - point(halfPoint[0] + HALF, halfPoint[1] - QUARTER), + pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER), + pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER), context, ); drawLine( - point(halfPoint[0] - HALF, halfPoint[1] + QUARTER), - point(halfPoint[0] + HALF, halfPoint[1] + QUARTER), + pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER), + pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER), context, ); if (!appState.zenModeEnabled) { // (4) - drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context); + drawLine( + pointFrom(to[0] - FULL, to[1]), + pointFrom(to[0] + FULL, to[1]), + context, + ); // (2) drawLine(from, to, context); diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index 19169d4a9..f0bf98967 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -421,6 +421,7 @@ const renderElementToSvg = ( image.setAttribute("width", "100%"); image.setAttribute("height", "100%"); image.setAttribute("href", fileData.dataURL); + image.setAttribute("preserveAspectRatio", "none"); symbol.appendChild(image); diff --git a/packages/excalidraw/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts index fad0f4f93..0426b3f70 100644 --- a/packages/excalidraw/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -24,7 +24,7 @@ import { import { canChangeRoundness } from "./comparisons"; import type { EmbedsValidationStatus } from "../types"; import { - point, + pointFrom, pointDistance, type GlobalPoint, type LocalPoint, @@ -408,7 +408,7 @@ export const _generateElementShape = ( // initial position to it const points = element.points.length ? element.points - : [point (0, 0)]; + : [pointFrom (0, 0)]; if (isElbowArrow(element)) { shape = [ diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index b120d0cc9..6d1b963fc 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -185,6 +185,11 @@ export const exportToCanvas = async ( exportingFrame ?? null, appState.frameRendering ?? null, ); + // for canvas export, don't clip if exporting a specific frame as it would + // clip the corners of the content + if (exportingFrame) { + frameRendering.clip = false; + } const elementsForRender = prepareElementsForRender({ elements, @@ -351,6 +356,11 @@ export const exportToSvg = async ( }) rotate(${frame.angle} ${cx} ${cy})" width="${frame.width}" height="${frame.height}" + ${ + exportingFrame + ? "" + : `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}` + } > `; diff --git a/packages/excalidraw/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts index f3d6ac014..5d059e5b4 100644 --- a/packages/excalidraw/scene/scroll.ts +++ b/packages/excalidraw/scene/scroll.ts @@ -1,4 +1,4 @@ -import type { AppState, PointerCoords, Zoom } from "../types"; +import type { AppState, Offsets, PointerCoords, Zoom } from "../types"; import type { ExcalidrawElement } from "../element/types"; import { getCommonBounds, @@ -31,14 +31,28 @@ export const centerScrollOn = ({ scenePoint, viewportDimensions, zoom, + offsets, }: { scenePoint: PointerCoords; viewportDimensions: { height: number; width: number }; zoom: Zoom; + offsets?: Offsets; }) => { + let scrollX = + (viewportDimensions.width - (offsets?.right ?? 0)) / 2 / zoom.value - + scenePoint.x; + + scrollX += (offsets?.left ?? 0) / 2 / zoom.value; + + let scrollY = + (viewportDimensions.height - (offsets?.bottom ?? 0)) / 2 / zoom.value - + scenePoint.y; + + scrollY += (offsets?.top ?? 0) / 2 / zoom.value; + return { - scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x, - scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y, + scrollX, + scrollY, }; }; diff --git a/packages/excalidraw/shapes.tsx b/packages/excalidraw/shapes.tsx index 2c935145c..3f1855c63 100644 --- a/packages/excalidraw/shapes.tsx +++ b/packages/excalidraw/shapes.tsx @@ -1,6 +1,6 @@ import { isPoint, - point, + pointFrom, pointDistance, pointFromPair, pointRotateRads, @@ -167,15 +167,15 @@ export const getElementShape = ( ? getClosedCurveShape ( element, roughShape, - point (element.x, element.y), + pointFrom (element.x, element.y), element.angle, - point(cx, cy), + pointFrom(cx, cy), ) : getCurveShape ( roughShape, - point (element.x, element.y), + pointFrom (element.x, element.y), element.angle, - point(cx, cy), + pointFrom(cx, cy), ); } @@ -186,7 +186,7 @@ export const getElementShape = ( const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); return getFreedrawShape( element, - point(cx, cy), + pointFrom(cx, cy), shouldTestInside(element), ); } @@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = < } const ops = getCurvePathOps(shape[0]); - let currentP = point (0, 0); + let currentP = pointFrom
(0, 0); let index = 0; let minDistance = Infinity; let controlPoints: P[] | null = null; @@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = < } if (op === "bcurveTo") { const p0 = currentP; - const p1 = point
(data[0], data[1]); - const p2 = point
(data[2], data[3]); - const p3 = point
(data[4], data[5]); + const p1 = pointFrom
(data[0], data[1]); + const p2 = pointFrom
(data[2], data[3]); + const p3 = pointFrom
(data[4], data[5]); const distance = pointDistance(p3, endPoint); if (distance < minDistance) { minDistance = distance; @@ -279,7 +279,7 @@ export const getBezierXY =
( p0[idx] * Math.pow(t, 3); const tx = equation(t, 0); const ty = equation(t, 1); - return point(tx, ty); + return pointFrom(tx, ty); }; const getPointsInBezierCurve =
( @@ -301,12 +301,12 @@ const getPointsInBezierCurve =
( controlPoints[3], t, ); - pointsOnCurve.push(point(p[0], p[1])); + pointsOnCurve.push(pointFrom(p[0], p[1])); t -= 0.05; } if (pointsOnCurve.length) { if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) { - pointsOnCurve.push(point(endPoint[0], endPoint[1])); + pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1])); } } return pointsOnCurve; @@ -393,24 +393,24 @@ export const aabbForElement = ( midY: element.y + element.height / 2, }; - const center = point(bbox.midX, bbox.midY); + const center = pointFrom(bbox.midX, bbox.midY); const [topLeftX, topLeftY] = pointRotateRads( - point(bbox.minX, bbox.minY), + pointFrom(bbox.minX, bbox.minY), center, element.angle, ); const [topRightX, topRightY] = pointRotateRads( - point(bbox.maxX, bbox.minY), + pointFrom(bbox.maxX, bbox.minY), center, element.angle, ); const [bottomRightX, bottomRightY] = pointRotateRads( - point(bbox.maxX, bbox.maxY), + pointFrom(bbox.maxX, bbox.maxY), center, element.angle, ); const [bottomLeftX, bottomLeftY] = pointRotateRads( - point(bbox.minX, bbox.maxY), + pointFrom(bbox.minX, bbox.maxY), center, element.angle, ); @@ -442,14 +442,14 @@ export const pointInsideBounds =
( p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; export const aabbsOverlapping = (a: Bounds, b: Bounds) => - pointInsideBounds(point(a[0], a[1]), b) || - pointInsideBounds(point(a[2], a[1]), b) || - pointInsideBounds(point(a[2], a[3]), b) || - pointInsideBounds(point(a[0], a[3]), b) || - pointInsideBounds(point(b[0], b[1]), a) || - pointInsideBounds(point(b[2], b[1]), a) || - pointInsideBounds(point(b[2], b[3]), a) || - pointInsideBounds(point(b[0], b[3]), a); + pointInsideBounds(pointFrom(a[0], a[1]), b) || + pointInsideBounds(pointFrom(a[2], a[1]), b) || + pointInsideBounds(pointFrom(a[2], a[3]), b) || + pointInsideBounds(pointFrom(a[0], a[3]), b) || + pointInsideBounds(pointFrom(b[0], b[1]), a) || + pointInsideBounds(pointFrom(b[2], b[1]), a) || + pointInsideBounds(pointFrom(b[2], b[3]), a) || + pointInsideBounds(pointFrom(b[0], b[3]), a); export const getCornerRadius = (x: number, element: ExcalidrawElement) => { if ( diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 9da3d74c4..1f2451b33 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -1,6 +1,6 @@ import type { InclusiveRange } from "../math"; import { - point, + pointFrom, pointRotateRads, rangeInclusive, rangeIntersection, @@ -228,52 +228,52 @@ export const getElementsCorners = ( !boundingBoxCorners ) { const leftMid = pointRotateRads
( - point(x1, y1 + halfHeight), - point(cx, cy), + pointFrom(x1, y1 + halfHeight), + pointFrom(cx, cy), element.angle, ); const topMid = pointRotateRads ( - point(x1 + halfWidth, y1), - point(cx, cy), + pointFrom(x1 + halfWidth, y1), + pointFrom(cx, cy), element.angle, ); const rightMid = pointRotateRads ( - point(x2, y1 + halfHeight), - point(cx, cy), + pointFrom(x2, y1 + halfHeight), + pointFrom(cx, cy), element.angle, ); const bottomMid = pointRotateRads ( - point(x1 + halfWidth, y2), - point(cx, cy), + pointFrom(x1 + halfWidth, y2), + pointFrom(cx, cy), element.angle, ); - const center = point (cx, cy); + const center = pointFrom (cx, cy); result = omitCenter ? [leftMid, topMid, rightMid, bottomMid] : [leftMid, topMid, rightMid, bottomMid, center]; } else { const topLeft = pointRotateRads ( - point(x1, y1), - point(cx, cy), + pointFrom(x1, y1), + pointFrom(cx, cy), element.angle, ); const topRight = pointRotateRads ( - point(x2, y1), - point(cx, cy), + pointFrom(x2, y1), + pointFrom(cx, cy), element.angle, ); const bottomLeft = pointRotateRads ( - point(x1, y2), - point(cx, cy), + pointFrom(x1, y2), + pointFrom(cx, cy), element.angle, ); const bottomRight = pointRotateRads ( - point(x2, y2), - point(cx, cy), + pointFrom(x2, y2), + pointFrom(cx, cy), element.angle, ); - const center = point (cx, cy); + const center = pointFrom (cx, cy); result = omitCenter ? [topLeft, topRight, bottomLeft, bottomRight] @@ -287,18 +287,18 @@ export const getElementsCorners = ( const width = maxX - minX; const height = maxY - minY; - const topLeft = point (minX, minY); - const topRight = point (maxX, minY); - const bottomLeft = point (minX, maxY); - const bottomRight = point (maxX, maxY); - const center = point (minX + width / 2, minY + height / 2); + const topLeft = pointFrom (minX, minY); + const topRight = pointFrom (maxX, minY); + const bottomLeft = pointFrom (minX, maxY); + const bottomRight = pointFrom (maxX, maxY); + const center = pointFrom (minX + width / 2, minY + height / 2); result = omitCenter ? [topLeft, topRight, bottomLeft, bottomRight] : [topLeft, topRight, bottomLeft, bottomRight, center]; } - return result.map((p) => point(round(p[0]), round(p[1]))); + return result.map((p) => pointFrom(round(p[0]), round(p[1]))); }; const getReferenceElements = ( @@ -375,8 +375,11 @@ export const getVisibleGaps = ( horizontalGaps.push({ startBounds, endBounds, - startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)], - endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)], + startSide: [ + pointFrom(startMaxX, startMinY), + pointFrom(startMaxX, startMaxY), + ], + endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)], length: endMinX - startMaxX, overlap: rangeIntersection( rangeInclusive(startMinY, startMaxY), @@ -415,8 +418,11 @@ export const getVisibleGaps = ( verticalGaps.push({ startBounds, endBounds, - startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)], - endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)], + startSide: [ + pointFrom(startMinX, startMaxY), + pointFrom(startMaxX, startMaxY), + ], + endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)], length: endMinY - startMaxY, overlap: rangeIntersection( rangeInclusive(startMinX, startMaxX), @@ -832,7 +838,7 @@ const createPointSnapLines = ( } snapsX[key].push( ...snap.points.map((p) => - point (round(p[0]), round(p[1])), + pointFrom (round(p[0]), round(p[1])), ), ); } @@ -849,7 +855,7 @@ const createPointSnapLines = ( } snapsY[key].push( ...snap.points.map((p) => - point (round(p[0]), round(p[1])), + pointFrom (round(p[0]), round(p[1])), ), ); } @@ -863,7 +869,7 @@ const createPointSnapLines = ( points: dedupePoints( points .map((p) => { - return point (Number(key), p[1]); + return pointFrom (Number(key), p[1]); }) .sort((a, b) => a[1] - b[1]), ), @@ -876,7 +882,7 @@ const createPointSnapLines = ( points: dedupePoints( points .map((p) => { - return point (p[0], Number(key)); + return pointFrom (p[0], Number(key)); }) .sort((a, b) => a[0] - b[0]), ), @@ -940,16 +946,16 @@ const createGapSnapLines = ( type: "gap", direction: "horizontal", points: [ - point(gapSnap.gap.startSide[0][0], gapLineY), - point(minX, gapLineY), + pointFrom(gapSnap.gap.startSide[0][0], gapLineY), + pointFrom(minX, gapLineY), ], }, { type: "gap", direction: "horizontal", points: [ - point(maxX, gapLineY), - point(gapSnap.gap.endSide[0][0], gapLineY), + pointFrom(maxX, gapLineY), + pointFrom(gapSnap.gap.endSide[0][0], gapLineY), ], }, ); @@ -966,16 +972,16 @@ const createGapSnapLines = ( type: "gap", direction: "vertical", points: [ - point(gapLineX, gapSnap.gap.startSide[0][1]), - point(gapLineX, minY), + pointFrom(gapLineX, gapSnap.gap.startSide[0][1]), + pointFrom(gapLineX, minY), ], }, { type: "gap", direction: "vertical", points: [ - point(gapLineX, maxY), - point(gapLineX, gapSnap.gap.endSide[0][1]), + pointFrom(gapLineX, maxY), + pointFrom(gapLineX, gapSnap.gap.endSide[0][1]), ], }, ); @@ -991,12 +997,15 @@ const createGapSnapLines = ( { type: "gap", direction: "horizontal", - points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], + points: [ + pointFrom(startMaxX, gapLineY), + pointFrom(endMinX, gapLineY), + ], }, { type: "gap", direction: "horizontal", - points: [point(endMaxX, gapLineY), point(minX, gapLineY)], + points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)], }, ); } @@ -1011,12 +1020,18 @@ const createGapSnapLines = ( { type: "gap", direction: "horizontal", - points: [point(maxX, gapLineY), point(startMinX, gapLineY)], + points: [ + pointFrom(maxX, gapLineY), + pointFrom(startMinX, gapLineY), + ], }, { type: "gap", direction: "horizontal", - points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], + points: [ + pointFrom(startMaxX, gapLineY), + pointFrom(endMinX, gapLineY), + ], }, ); } @@ -1031,12 +1046,18 @@ const createGapSnapLines = ( { type: "gap", direction: "vertical", - points: [point(gapLineX, maxY), point(gapLineX, startMinY)], + points: [ + pointFrom(gapLineX, maxY), + pointFrom(gapLineX, startMinY), + ], }, { type: "gap", direction: "vertical", - points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], + points: [ + pointFrom(gapLineX, startMaxY), + pointFrom(gapLineX, endMinY), + ], }, ); } @@ -1051,12 +1072,15 @@ const createGapSnapLines = ( { type: "gap", direction: "vertical", - points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], + points: [ + pointFrom(gapLineX, startMaxY), + pointFrom(gapLineX, endMinY), + ], }, { type: "gap", direction: "vertical", - points: [point(gapLineX, endMaxY), point(gapLineX, minY)], + points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)], }, ); } @@ -1070,7 +1094,7 @@ const createGapSnapLines = ( return { ...gapSnapLine, points: gapSnapLine.points.map((p) => - point(round(p[0]), round(p[1])), + pointFrom(round(p[0]), round(p[1])), ) as PointPair, }; }), @@ -1120,35 +1144,35 @@ export const snapResizingElements = ( if (transformHandle) { switch (transformHandle) { case "e": { - selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY)); break; } case "w": { - selectionSnapPoints.push(point(minX, minY), point(minX, maxY)); + selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY)); break; } case "n": { - selectionSnapPoints.push(point(minX, minY), point(maxX, minY)); + selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY)); break; } case "s": { - selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY)); break; } case "ne": { - selectionSnapPoints.push(point(maxX, minY)); + selectionSnapPoints.push(pointFrom(maxX, minY)); break; } case "nw": { - selectionSnapPoints.push(point(minX, minY)); + selectionSnapPoints.push(pointFrom(minX, minY)); break; } case "se": { - selectionSnapPoints.push(point(maxX, maxY)); + selectionSnapPoints.push(pointFrom(maxX, maxY)); break; } case "sw": { - selectionSnapPoints.push(point(minX, maxY)); + selectionSnapPoints.push(pointFrom(minX, maxY)); break; } } @@ -1191,10 +1215,10 @@ export const snapResizingElements = ( ); const corners: GlobalPoint[] = [ - point(x1, y1), - point(x1, y2), - point(x2, y1), - point(x2, y2), + pointFrom(x1, y1), + pointFrom(x1, y2), + pointFrom(x2, y1), + pointFrom(x2, y2), ]; getPointSnaps( @@ -1231,7 +1255,7 @@ export const snapNewElement = ( } const selectionSnapPoints: GlobalPoint[] = [ - point(origin.x + dragOffset.x, origin.y + dragOffset.y), + pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y), ]; const snapDistance = getSnapDistance(app.state.zoom.value); @@ -1331,7 +1355,7 @@ export const getSnapLinesAtPointer = ( verticalSnapLines.push({ type: "pointer", - points: [corner, point(corner[0], pointer.y)], + points: [corner, pointFrom(corner[0], pointer.y)], direction: "vertical", }); @@ -1347,7 +1371,7 @@ export const getSnapLinesAtPointer = ( horizontalSnapLines.push({ type: "pointer", - points: [corner, point(pointer.x, corner[1])], + points: [corner, pointFrom(pointer.x, corner[1])], direction: "horizontal", }); diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 7f9904a4d..f481d1d5d 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -794,6 +794,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "left": 30, "top": 40, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -836,6 +837,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -866,6 +868,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -999,6 +1002,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -1041,6 +1045,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -1068,6 +1073,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1214,6 +1220,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -1256,6 +1263,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -1283,6 +1291,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1544,6 +1553,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -1586,6 +1596,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -1613,6 +1624,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -1874,6 +1886,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -1916,6 +1929,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -1943,6 +1957,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -2089,6 +2104,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -2131,6 +2147,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -2158,6 +2175,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2328,6 +2346,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -2370,6 +2389,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -2397,6 +2417,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0_copy": true, }, @@ -2628,6 +2649,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -2670,6 +2692,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -2699,6 +2722,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -2996,6 +3020,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -3038,6 +3063,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -3065,6 +3091,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -3470,6 +3497,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -3512,6 +3540,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -3539,6 +3568,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, @@ -3792,6 +3822,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -3834,6 +3865,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -3861,6 +3893,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, @@ -4114,6 +4147,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung }, "collaborators": Map {}, "contextMenu": null, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -4156,6 +4190,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -4185,6 +4220,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -5299,6 +5335,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "left": -17, "top": -7, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -5341,6 +5378,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -5370,6 +5408,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -6425,6 +6464,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "left": -17, "top": -7, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -6467,6 +6507,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -6496,6 +6537,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, "id1": true, @@ -7359,6 +7401,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "left": -19, "top": -9, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -7401,6 +7444,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -7431,6 +7475,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8270,6 +8315,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "left": -17, "top": -7, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -8312,6 +8358,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -8339,6 +8386,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id0": true, }, @@ -9163,6 +9211,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "left": 80, "top": 90, }, + "croppingElement": null, "currentChartType": "bar", "currentHoveredFontFamily": null, "currentItemArrowType": "round", @@ -9205,6 +9254,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "gridStep": 5, "height": 100, "isBindingEnabled": true, + "isCropping": false, "isLoading": false, "isResizing": false, "isRotating": false, @@ -9235,6 +9285,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "scrollX": 0, "scrollY": 0, "scrolledOutside": false, + "searchMatches": [], "selectedElementIds": { "id1": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap index 2994cfc3e..e5e431dfc 100644 --- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap @@ -239,6 +239,55 @@ exports[` > Test UIOptions prop > Test canvasActions > should rende Ctrl+Shift+E