Compare commits
7 Commits
master
...
ryan-di/el
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d54e3446 | ||
|
|
f93b040fff | ||
|
|
53cae28d2a | ||
|
|
70e515f560 | ||
|
|
5516e7c819 | ||
|
|
6eb93b281a | ||
|
|
2a50000ec8 |
@ -56,6 +56,7 @@ import {
|
|||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
isRectangularElement,
|
||||||
isRectanguloidElement,
|
isRectanguloidElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
@ -63,6 +64,11 @@ import {
|
|||||||
import { aabbForElement, elementCenterPoint } from "./bounds";
|
import { aabbForElement, elementCenterPoint } from "./bounds";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
@ -82,6 +88,7 @@ import type {
|
|||||||
FixedPoint,
|
FixedPoint,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
PointsPositionUpdates,
|
PointsPositionUpdates,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
@ -2279,3 +2286,434 @@ export const normalizeFixedPoint = <T extends FixedPoint | null>(
|
|||||||
}
|
}
|
||||||
return fixedPoint as any as T extends null ? null : FixedPoint;
|
return fixedPoint as any as T extends null ? null : FixedPoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Side =
|
||||||
|
| "top"
|
||||||
|
| "top-right"
|
||||||
|
| "right"
|
||||||
|
| "bottom-right"
|
||||||
|
| "bottom"
|
||||||
|
| "bottom-left"
|
||||||
|
| "left"
|
||||||
|
| "top-left";
|
||||||
|
type ShapeType = "rectangle" | "ellipse" | "diamond";
|
||||||
|
const getShapeType = (element: ExcalidrawBindableElement): ShapeType => {
|
||||||
|
if (element.type === "ellipse" || element.type === "diamond") {
|
||||||
|
return element.type;
|
||||||
|
}
|
||||||
|
return "rectangle";
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SectorConfig {
|
||||||
|
// center angle of the sector in degrees
|
||||||
|
centerAngle: number;
|
||||||
|
// width of the sector in degrees
|
||||||
|
sectorWidth: number;
|
||||||
|
side: Side;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define sector configurations for different shape types
|
||||||
|
const SHAPE_CONFIGS: Record<ShapeType, SectorConfig[]> = {
|
||||||
|
// rectangle: 15° corners, 75° edges
|
||||||
|
rectangle: [
|
||||||
|
{ centerAngle: 0, sectorWidth: 75, side: "right" },
|
||||||
|
{ centerAngle: 45, sectorWidth: 15, side: "bottom-right" },
|
||||||
|
{ centerAngle: 90, sectorWidth: 75, side: "bottom" },
|
||||||
|
{ centerAngle: 135, sectorWidth: 15, side: "bottom-left" },
|
||||||
|
{ centerAngle: 180, sectorWidth: 75, side: "left" },
|
||||||
|
{ centerAngle: 225, sectorWidth: 15, side: "top-left" },
|
||||||
|
{ centerAngle: 270, sectorWidth: 75, side: "top" },
|
||||||
|
{ centerAngle: 315, sectorWidth: 15, side: "top-right" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// diamond: 15° vertices, 75° edges
|
||||||
|
diamond: [
|
||||||
|
{ centerAngle: 0, sectorWidth: 15, side: "right" },
|
||||||
|
{ centerAngle: 45, sectorWidth: 75, side: "bottom-right" },
|
||||||
|
{ centerAngle: 90, sectorWidth: 15, side: "bottom" },
|
||||||
|
{ centerAngle: 135, sectorWidth: 75, side: "bottom-left" },
|
||||||
|
{ centerAngle: 180, sectorWidth: 15, side: "left" },
|
||||||
|
{ centerAngle: 225, sectorWidth: 75, side: "top-left" },
|
||||||
|
{ centerAngle: 270, sectorWidth: 15, side: "top" },
|
||||||
|
{ centerAngle: 315, sectorWidth: 75, side: "top-right" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// ellipse: 15° cardinal points, 75° diagonals
|
||||||
|
ellipse: [
|
||||||
|
{ centerAngle: 0, sectorWidth: 15, side: "right" },
|
||||||
|
{ centerAngle: 45, sectorWidth: 75, side: "bottom-right" },
|
||||||
|
{ centerAngle: 90, sectorWidth: 15, side: "bottom" },
|
||||||
|
{ centerAngle: 135, sectorWidth: 75, side: "bottom-left" },
|
||||||
|
{ centerAngle: 180, sectorWidth: 15, side: "left" },
|
||||||
|
{ centerAngle: 225, sectorWidth: 75, side: "top-left" },
|
||||||
|
{ centerAngle: 270, sectorWidth: 15, side: "top" },
|
||||||
|
{ centerAngle: 315, sectorWidth: 75, side: "top-right" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSectorBoundaries = (
|
||||||
|
config: SectorConfig[],
|
||||||
|
): Array<{ start: number; end: number; side: Side }> => {
|
||||||
|
return config.map((sector, index) => {
|
||||||
|
const halfWidth = sector.sectorWidth / 2;
|
||||||
|
let start = sector.centerAngle - halfWidth;
|
||||||
|
let end = sector.centerAngle + halfWidth;
|
||||||
|
|
||||||
|
// normalize angles to [0, 360) range
|
||||||
|
start = ((start % 360) + 360) % 360;
|
||||||
|
end = ((end % 360) + 360) % 360;
|
||||||
|
|
||||||
|
return { start, end, side: sector.side };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// determine which side a point falls into using adaptive sectors
|
||||||
|
const getShapeSideAdaptive = (
|
||||||
|
fixedPoint: FixedPoint,
|
||||||
|
shapeType: ShapeType,
|
||||||
|
): Side => {
|
||||||
|
const [x, y] = fixedPoint;
|
||||||
|
|
||||||
|
// convert to centered coordinates
|
||||||
|
const centerX = x - 0.5;
|
||||||
|
const centerY = y - 0.5;
|
||||||
|
|
||||||
|
// calculate angle
|
||||||
|
let angle = Math.atan2(centerY, centerX);
|
||||||
|
if (angle < 0) {
|
||||||
|
angle += 2 * Math.PI;
|
||||||
|
}
|
||||||
|
const degrees = (angle * 180) / Math.PI;
|
||||||
|
|
||||||
|
// get sector configuration for this shape type
|
||||||
|
const config = SHAPE_CONFIGS[shapeType];
|
||||||
|
const boundaries = getSectorBoundaries(config);
|
||||||
|
|
||||||
|
// find which sector the angle falls into
|
||||||
|
for (const boundary of boundaries) {
|
||||||
|
if (boundary.start <= boundary.end) {
|
||||||
|
// Normal case: sector doesn't cross 0°
|
||||||
|
if (degrees >= boundary.start && degrees <= boundary.end) {
|
||||||
|
return boundary.side;
|
||||||
|
}
|
||||||
|
} else if (degrees >= boundary.start || degrees <= boundary.end) {
|
||||||
|
return boundary.side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback - find nearest sector center
|
||||||
|
let minDiff = Infinity;
|
||||||
|
let nearestSide = config[0].side;
|
||||||
|
|
||||||
|
for (const sector of config) {
|
||||||
|
let diff = Math.abs(degrees - sector.centerAngle);
|
||||||
|
// handle wraparound
|
||||||
|
if (diff > 180) {
|
||||||
|
diff = 360 - diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff;
|
||||||
|
nearestSide = sector.side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearestSide;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBindingSideMidPoint = (
|
||||||
|
binding: FixedPointBinding,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
const bindableElement = elementsMap.get(binding.elementId);
|
||||||
|
if (
|
||||||
|
!bindableElement ||
|
||||||
|
bindableElement.isDeleted ||
|
||||||
|
!isBindableElement(bindableElement)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = elementCenterPoint(bindableElement, elementsMap);
|
||||||
|
const shapeType = getShapeType(bindableElement);
|
||||||
|
const side = getShapeSideAdaptive(
|
||||||
|
normalizeFixedPoint(binding.fixedPoint),
|
||||||
|
shapeType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// small offset to avoid precision issues in elbow
|
||||||
|
const OFFSET = 0.01;
|
||||||
|
|
||||||
|
if (bindableElement.type === "diamond") {
|
||||||
|
const [sides, corners] = deconstructDiamondElement(bindableElement);
|
||||||
|
const [bottomRight, bottomLeft, topLeft, topRight] = sides;
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
switch (side) {
|
||||||
|
case "left": {
|
||||||
|
// left vertex - use the center of the left corner curve
|
||||||
|
if (corners.length >= 3) {
|
||||||
|
const leftCorner = corners[2];
|
||||||
|
const midPoint = leftCorner[1];
|
||||||
|
x = midPoint[0] - OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
} else {
|
||||||
|
// fallback for non-rounded diamond
|
||||||
|
const midPoint = getMidPoint(bottomLeft[1], topLeft[0]);
|
||||||
|
x = midPoint[0] - OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "right": {
|
||||||
|
if (corners.length >= 1) {
|
||||||
|
const rightCorner = corners[0];
|
||||||
|
const midPoint = rightCorner[1];
|
||||||
|
x = midPoint[0] + OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
} else {
|
||||||
|
const midPoint = getMidPoint(topRight[1], bottomRight[0]);
|
||||||
|
x = midPoint[0] + OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top": {
|
||||||
|
if (corners.length >= 4) {
|
||||||
|
const topCorner = corners[3];
|
||||||
|
const midPoint = topCorner[1];
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] - OFFSET;
|
||||||
|
} else {
|
||||||
|
const midPoint = getMidPoint(topLeft[1], topRight[0]);
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] - OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom": {
|
||||||
|
if (corners.length >= 2) {
|
||||||
|
const bottomCorner = corners[1];
|
||||||
|
const midPoint = bottomCorner[1];
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] + OFFSET;
|
||||||
|
} else {
|
||||||
|
const midPoint = getMidPoint(bottomRight[1], bottomLeft[0]);
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] + OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-right": {
|
||||||
|
const midPoint = getMidPoint(topRight[0], topRight[1]);
|
||||||
|
|
||||||
|
x = midPoint[0] + OFFSET * 0.707;
|
||||||
|
y = midPoint[1] - OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-right": {
|
||||||
|
const midPoint = getMidPoint(bottomRight[0], bottomRight[1]);
|
||||||
|
|
||||||
|
x = midPoint[0] + OFFSET * 0.707;
|
||||||
|
y = midPoint[1] + OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-left": {
|
||||||
|
const midPoint = getMidPoint(bottomLeft[0], bottomLeft[1]);
|
||||||
|
x = midPoint[0] - OFFSET * 0.707;
|
||||||
|
y = midPoint[1] + OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-left": {
|
||||||
|
const midPoint = getMidPoint(topLeft[0], topLeft[1]);
|
||||||
|
x = midPoint[0] - OFFSET * 0.707;
|
||||||
|
y = midPoint[1] - OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindableElement.type === "ellipse") {
|
||||||
|
const ellipseCenterX = bindableElement.x + bindableElement.width / 2;
|
||||||
|
const ellipseCenterY = bindableElement.y + bindableElement.height / 2;
|
||||||
|
const radiusX = bindableElement.width / 2;
|
||||||
|
const radiusY = bindableElement.height / 2;
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
|
||||||
|
switch (side) {
|
||||||
|
case "top": {
|
||||||
|
x = ellipseCenterX;
|
||||||
|
y = ellipseCenterY - radiusY - OFFSET;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "right": {
|
||||||
|
x = ellipseCenterX + radiusX + OFFSET;
|
||||||
|
y = ellipseCenterY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom": {
|
||||||
|
x = ellipseCenterX;
|
||||||
|
y = ellipseCenterY + radiusY + OFFSET;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "left": {
|
||||||
|
x = ellipseCenterX - radiusX - OFFSET;
|
||||||
|
y = ellipseCenterY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-right": {
|
||||||
|
const angle = -Math.PI / 4;
|
||||||
|
const ellipseX = radiusX * Math.cos(angle);
|
||||||
|
const ellipseY = radiusY * Math.sin(angle);
|
||||||
|
x = ellipseCenterX + ellipseX + OFFSET * 0.707;
|
||||||
|
y = ellipseCenterY + ellipseY - OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-right": {
|
||||||
|
const angle = Math.PI / 4;
|
||||||
|
const ellipseX = radiusX * Math.cos(angle);
|
||||||
|
const ellipseY = radiusY * Math.sin(angle);
|
||||||
|
x = ellipseCenterX + ellipseX + OFFSET * 0.707;
|
||||||
|
y = ellipseCenterY + ellipseY + OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-left": {
|
||||||
|
const angle = (3 * Math.PI) / 4;
|
||||||
|
const ellipseX = radiusX * Math.cos(angle);
|
||||||
|
const ellipseY = radiusY * Math.sin(angle);
|
||||||
|
x = ellipseCenterX + ellipseX - OFFSET * 0.707;
|
||||||
|
y = ellipseCenterY + ellipseY + OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-left": {
|
||||||
|
const angle = (-3 * Math.PI) / 4;
|
||||||
|
const ellipseX = radiusX * Math.cos(angle);
|
||||||
|
const ellipseY = radiusY * Math.sin(angle);
|
||||||
|
x = ellipseCenterX + ellipseX - OFFSET * 0.707;
|
||||||
|
y = ellipseCenterY + ellipseY - OFFSET * 0.707;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRectangularElement(bindableElement)) {
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(
|
||||||
|
bindableElement as ExcalidrawRectanguloidElement,
|
||||||
|
);
|
||||||
|
const [top, right, bottom, left] = sides;
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
switch (side) {
|
||||||
|
case "top": {
|
||||||
|
const midPoint = getMidPoint(top[0], top[1]);
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] - OFFSET;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "right": {
|
||||||
|
const midPoint = getMidPoint(right[0], right[1]);
|
||||||
|
x = midPoint[0] + OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom": {
|
||||||
|
const midPoint = getMidPoint(bottom[0], bottom[1]);
|
||||||
|
x = midPoint[0];
|
||||||
|
y = midPoint[1] + OFFSET;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "left": {
|
||||||
|
const midPoint = getMidPoint(left[0], left[1]);
|
||||||
|
x = midPoint[0] - OFFSET;
|
||||||
|
y = midPoint[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-left": {
|
||||||
|
if (corners.length >= 1) {
|
||||||
|
const corner = corners[0];
|
||||||
|
|
||||||
|
const p1 = corner[0];
|
||||||
|
const p2 = corner[3];
|
||||||
|
const midPoint = getMidPoint(p1, p2);
|
||||||
|
|
||||||
|
x = midPoint[0] - OFFSET * 0.707;
|
||||||
|
y = midPoint[1] - OFFSET * 0.707;
|
||||||
|
} else {
|
||||||
|
x = bindableElement.x - OFFSET;
|
||||||
|
y = bindableElement.y - OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "top-right": {
|
||||||
|
if (corners.length >= 2) {
|
||||||
|
const corner = corners[1];
|
||||||
|
const p1 = corner[0];
|
||||||
|
const p2 = corner[3];
|
||||||
|
const midPoint = getMidPoint(p1, p2);
|
||||||
|
|
||||||
|
x = midPoint[0] + OFFSET * 0.707;
|
||||||
|
y = midPoint[1] - OFFSET * 0.707;
|
||||||
|
} else {
|
||||||
|
x = bindableElement.x + bindableElement.width + OFFSET;
|
||||||
|
y = bindableElement.y - OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-right": {
|
||||||
|
if (corners.length >= 3) {
|
||||||
|
const corner = corners[2];
|
||||||
|
const p1 = corner[0];
|
||||||
|
const p2 = corner[3];
|
||||||
|
const midPoint = getMidPoint(p1, p2);
|
||||||
|
|
||||||
|
x = midPoint[0] + OFFSET * 0.707;
|
||||||
|
y = midPoint[1] + OFFSET * 0.707;
|
||||||
|
} else {
|
||||||
|
x = bindableElement.x + bindableElement.width + OFFSET;
|
||||||
|
y = bindableElement.y + bindableElement.height + OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "bottom-left": {
|
||||||
|
if (corners.length >= 4) {
|
||||||
|
const corner = corners[3];
|
||||||
|
const p1 = corner[0];
|
||||||
|
const p2 = corner[3];
|
||||||
|
const midPoint = getMidPoint(p1, p2);
|
||||||
|
|
||||||
|
x = midPoint[0] - OFFSET * 0.707;
|
||||||
|
y = midPoint[1] + OFFSET * 0.707;
|
||||||
|
} else {
|
||||||
|
x = bindableElement.x - OFFSET;
|
||||||
|
y = bindableElement.y + bindableElement.height + OFFSET;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMidPoint = (p1: GlobalPoint, p2: GlobalPoint): GlobalPoint => {
|
||||||
|
return pointFrom((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2);
|
||||||
|
};
|
||||||
|
|||||||
@ -366,15 +366,14 @@ export const adjustBoundTextSize = (
|
|||||||
container: ExcalidrawTextContainer,
|
container: ExcalidrawTextContainer,
|
||||||
boundText: ExcalidrawTextElementWithContainer,
|
boundText: ExcalidrawTextElementWithContainer,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
wrapTextFirst = true,
|
||||||
) => {
|
) => {
|
||||||
const maxWidth = getBoundTextMaxWidth(container, boundText);
|
const maxWidth = getBoundTextMaxWidth(container, boundText);
|
||||||
const maxHeight = getBoundTextMaxHeight(container, boundText);
|
const maxHeight = getBoundTextMaxHeight(container, boundText);
|
||||||
|
|
||||||
const wrappedText = wrapText(
|
const wrappedText = wrapTextFirst
|
||||||
boundText.text,
|
? wrapText(boundText.text, getFontString(boundText), maxWidth)
|
||||||
getFontString(boundText),
|
: boundText.originalText;
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
|
|
||||||
let metrics = measureText(
|
let metrics = measureText(
|
||||||
wrappedText,
|
wrappedText,
|
||||||
|
|||||||
@ -6,5 +6,19 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding-inline: 2.5rem;
|
padding-inline: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-arrow-type {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8;
|
||||||
|
// height: 2rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import { EDITOR_LS_KEYS, debounce, isDevEnv } from "@excalidraw/common";
|
|||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { useApp } from "../App";
|
import { useApp } from "../App";
|
||||||
import { ArrowRightIcon } from "../icons";
|
import { ArrowRightIcon, elbowArrowIcon, roundArrowIcon } from "../icons";
|
||||||
import { EditorLocalStorage } from "../../data/EditorLocalStorage";
|
import { EditorLocalStorage } from "../../data/EditorLocalStorage";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import Trans from "../Trans";
|
import Trans from "../Trans";
|
||||||
|
|
||||||
|
import { RadioGroup } from "../RadioGroup";
|
||||||
|
|
||||||
import { TTDDialogInput } from "./TTDDialogInput";
|
import { TTDDialogInput } from "./TTDDialogInput";
|
||||||
import { TTDDialogOutput } from "./TTDDialogOutput";
|
import { TTDDialogOutput } from "./TTDDialogOutput";
|
||||||
import { TTDDialogPanel } from "./TTDDialogPanel";
|
import { TTDDialogPanel } from "./TTDDialogPanel";
|
||||||
@ -43,6 +45,7 @@ const MermaidToExcalidraw = ({
|
|||||||
);
|
);
|
||||||
const deferredText = useDeferredValue(text.trim());
|
const deferredText = useDeferredValue(text.trim());
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [arrowType, setArrowType] = useState<"arrow" | "elbow">("arrow");
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
const data = useRef<{
|
const data = useRef<{
|
||||||
@ -59,6 +62,7 @@ const MermaidToExcalidraw = ({
|
|||||||
mermaidToExcalidrawLib,
|
mermaidToExcalidrawLib,
|
||||||
setError,
|
setError,
|
||||||
mermaidDefinition: deferredText,
|
mermaidDefinition: deferredText,
|
||||||
|
useElbow: arrowType === "elbow",
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (isDevEnv()) {
|
if (isDevEnv()) {
|
||||||
console.error("Failed to parse mermaid definition", err);
|
console.error("Failed to parse mermaid definition", err);
|
||||||
@ -66,7 +70,7 @@ const MermaidToExcalidraw = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
debouncedSaveMermaidDefinition(deferredText);
|
debouncedSaveMermaidDefinition(deferredText);
|
||||||
}, [deferredText, mermaidToExcalidrawLib]);
|
}, [deferredText, mermaidToExcalidrawLib, arrowType]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
@ -76,6 +80,19 @@ const MermaidToExcalidraw = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onInsertToEditor = () => {
|
const onInsertToEditor = () => {
|
||||||
|
convertMermaidToExcalidraw({
|
||||||
|
canvasRef,
|
||||||
|
data,
|
||||||
|
mermaidToExcalidrawLib,
|
||||||
|
setError,
|
||||||
|
mermaidDefinition: deferredText,
|
||||||
|
useElbow: arrowType === "elbow",
|
||||||
|
}).catch((err) => {
|
||||||
|
if (isDevEnv()) {
|
||||||
|
console.error("Failed to parse mermaid definition", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
insertToEditor({
|
insertToEditor({
|
||||||
app,
|
app,
|
||||||
data,
|
data,
|
||||||
@ -123,6 +140,29 @@ const MermaidToExcalidraw = ({
|
|||||||
icon: ArrowRightIcon,
|
icon: ArrowRightIcon,
|
||||||
}}
|
}}
|
||||||
renderSubmitShortcut={() => <TTDDialogSubmitShortcut />}
|
renderSubmitShortcut={() => <TTDDialogSubmitShortcut />}
|
||||||
|
renderBottomRight={() => (
|
||||||
|
<div className="dialog-mermaid-arrow-type">
|
||||||
|
<RadioGroup
|
||||||
|
name={"mermaid arrow config"}
|
||||||
|
value={arrowType}
|
||||||
|
onChange={(value: "arrow" | "elbow") => {
|
||||||
|
setArrowType(value);
|
||||||
|
}}
|
||||||
|
choices={[
|
||||||
|
{
|
||||||
|
value: "arrow",
|
||||||
|
label: roundArrowIcon,
|
||||||
|
ariaLabel: `${t("labels.arrowtype_round")}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "elbow",
|
||||||
|
label: elbowArrowIcon,
|
||||||
|
ariaLabel: `${t("labels.arrowtype_elbowed")}`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<TTDDialogOutput
|
<TTDDialogOutput
|
||||||
canvasRef={canvasRef}
|
canvasRef={canvasRef}
|
||||||
|
|||||||
@ -234,6 +234,11 @@ $verticalBreakpoint: 861px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ttd-dialog-panel-button-container-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.ttd-dialog-panel-button {
|
.ttd-dialog-panel-button {
|
||||||
&.excalidraw-button {
|
&.excalidraw-button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
@ -42,22 +42,28 @@ export const TTDDialogPanel = ({
|
|||||||
className={clsx("ttd-dialog-panel-button-container", {
|
className={clsx("ttd-dialog-panel-button-container", {
|
||||||
invisible: !panelAction,
|
invisible: !panelAction,
|
||||||
})}
|
})}
|
||||||
style={{ display: "flex", alignItems: "center" }}
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<div className="ttd-dialog-panel-button-container-left">
|
||||||
className="ttd-dialog-panel-button"
|
<Button
|
||||||
onSelect={panelAction ? panelAction.action : () => {}}
|
className="ttd-dialog-panel-button"
|
||||||
disabled={panelActionDisabled || onTextSubmitInProgess}
|
onSelect={panelAction ? panelAction.action : () => {}}
|
||||||
>
|
disabled={panelActionDisabled || onTextSubmitInProgess}
|
||||||
<div className={clsx({ invisible: onTextSubmitInProgess })}>
|
>
|
||||||
{panelAction?.label}
|
<div className={clsx({ invisible: onTextSubmitInProgess })}>
|
||||||
{panelAction?.icon && <span>{panelAction.icon}</span>}
|
{panelAction?.label}
|
||||||
</div>
|
{panelAction?.icon && <span>{panelAction.icon}</span>}
|
||||||
{onTextSubmitInProgess && <Spinner />}
|
</div>
|
||||||
</Button>
|
{onTextSubmitInProgess && <Spinner />}
|
||||||
{!panelActionDisabled &&
|
</Button>
|
||||||
!onTextSubmitInProgess &&
|
{!panelActionDisabled &&
|
||||||
renderSubmitShortcut?.()}
|
!onTextSubmitInProgess &&
|
||||||
|
renderSubmitShortcut?.()}
|
||||||
|
</div>
|
||||||
{renderBottomRight?.()}
|
{renderBottomRight?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -52,6 +52,7 @@ interface ConvertMermaidToExcalidrawFormatProps {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
}>;
|
}>;
|
||||||
|
useElbow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertMermaidToExcalidraw = async ({
|
export const convertMermaidToExcalidraw = async ({
|
||||||
@ -60,6 +61,7 @@ export const convertMermaidToExcalidraw = async ({
|
|||||||
mermaidDefinition,
|
mermaidDefinition,
|
||||||
setError,
|
setError,
|
||||||
data,
|
data,
|
||||||
|
useElbow = false,
|
||||||
}: ConvertMermaidToExcalidrawFormatProps) => {
|
}: ConvertMermaidToExcalidrawFormatProps) => {
|
||||||
const canvasNode = canvasRef.current;
|
const canvasNode = canvasRef.current;
|
||||||
const parent = canvasNode?.parentElement;
|
const parent = canvasNode?.parentElement;
|
||||||
@ -87,9 +89,11 @@ export const convertMermaidToExcalidraw = async ({
|
|||||||
const { elements, files } = ret;
|
const { elements, files } = ret;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Store the converted elements (which now include adjusted elbow arrow points)
|
||||||
data.current = {
|
data.current = {
|
||||||
elements: convertToExcalidrawElements(elements, {
|
elements: convertToExcalidrawElements(elements, {
|
||||||
regenerateIds: true,
|
regenerateIds: true,
|
||||||
|
useElbow,
|
||||||
}),
|
}),
|
||||||
files,
|
files,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -314,7 +314,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 560,
|
"x": 355,
|
||||||
"y": 226.5,
|
"y": 226.5,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -334,8 +334,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"focus": 0,
|
"focus": -0,
|
||||||
"gap": 16,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -2141,10 +2141,10 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 50,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a6",
|
"index": "a6",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2159,17 +2159,19 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"strokeColor": "#1098ad",
|
"strokeColor": "#1098ad",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"text": "ANOTHER STYLED
|
"text": "ANOTHER
|
||||||
LABELLED ARROW",
|
STYLED
|
||||||
|
LABELLE
|
||||||
|
D ARROW",
|
||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 140,
|
"width": 70,
|
||||||
"x": 80,
|
"x": 115,
|
||||||
"y": 275,
|
"y": 300,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2183,10 +2185,10 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 50,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a7",
|
"index": "a7",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2201,17 +2203,19 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"strokeColor": "#099268",
|
"strokeColor": "#099268",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"text": "ANOTHER STYLED
|
"text": "ANOTHER
|
||||||
LABELLED ARROW",
|
STYLED
|
||||||
|
LABELLE
|
||||||
|
D ARROW",
|
||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 140,
|
"width": 70,
|
||||||
"x": 80,
|
"x": 115,
|
||||||
"y": 375,
|
"y": 400,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2229,7 +2233,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 35,
|
"height": 10,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2266,7 +2270,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 85,
|
"height": 14,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2303,7 +2307,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 170,
|
"height": 20,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2340,7 +2344,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 120,
|
"height": 20,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2377,7 +2381,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 85,
|
"height": 10,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a4",
|
"index": "a4",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2414,7 +2418,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 120,
|
"height": 14,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a5",
|
"index": "a5",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2447,10 +2451,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 25,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a6",
|
"index": "a6",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2469,7 +2473,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 240,
|
"width": 240,
|
||||||
@ -2488,10 +2492,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 50,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a7",
|
"index": "a7",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2511,12 +2515,12 @@ CONTAINER",
|
|||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
"x": 539.7893218813452,
|
"x": 539.7893218813452,
|
||||||
"y": 117.44796179957173,
|
"y": 107.05025253169417,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2530,10 +2534,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 75,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a8",
|
"index": "a8",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2555,12 +2559,12 @@ CONTAINER",
|
|||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
"x": 195,
|
"x": 195,
|
||||||
"y": 197.5,
|
"y": 160,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2574,10 +2578,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 50,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a9",
|
"index": "a9",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2597,12 +2601,12 @@ TEXT CONTAINER",
|
|||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 140,
|
"width": 140,
|
||||||
"x": 180,
|
"x": 180,
|
||||||
"y": 435,
|
"y": 410,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2616,10 +2620,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 75,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "aA",
|
"index": "aA",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2640,7 +2644,7 @@ CONTAINER",
|
|||||||
"textAlign": "left",
|
"textAlign": "left",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "top",
|
"verticalAlign": "top",
|
||||||
"width": 160,
|
"width": 160,
|
||||||
@ -2659,10 +2663,10 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 5,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 0,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 75,
|
"height": 0,
|
||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "aB",
|
"index": "aB",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2683,11 +2687,11 @@ CONTAINER",
|
|||||||
"textAlign": "center",
|
"textAlign": "center",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": Any<Number>,
|
"versionNonce": Any<Number>,
|
||||||
"verticalAlign": "middle",
|
"verticalAlign": "middle",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
"x": 539.7893218813452,
|
"x": 539.7893218813452,
|
||||||
"y": 522.5735931288071,
|
"y": 507.0502525316942,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -16,7 +16,12 @@ import {
|
|||||||
getLineHeight,
|
getLineHeight,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { bindLinearElement } from "@excalidraw/element";
|
import {
|
||||||
|
bindLinearElement,
|
||||||
|
calculateFixedPointForElbowArrowBinding,
|
||||||
|
getBindingSideMidPoint,
|
||||||
|
isElbowArrow,
|
||||||
|
} from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
newElement,
|
newElement,
|
||||||
@ -31,8 +36,6 @@ import { isArrowElement } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { syncInvalidIndices } from "@excalidraw/element";
|
import { syncInvalidIndices } from "@excalidraw/element";
|
||||||
|
|
||||||
import { redrawTextBoundingBox } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element";
|
import { getCommonBounds } from "@excalidraw/element";
|
||||||
@ -63,6 +66,8 @@ import type {
|
|||||||
|
|
||||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { adjustBoundTextSize } from "../components/ConvertElementTypePopup";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
x: number;
|
x: number;
|
||||||
@ -238,247 +243,11 @@ const bindTextToContainer = (
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
redrawTextBoundingBox(textElement, container, scene);
|
adjustBoundTextSize(container as any, textElement as any, scene, false);
|
||||||
|
|
||||||
return [container, textElement] as const;
|
return [container, textElement] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindLinearElementToElement = (
|
|
||||||
linearElement: ExcalidrawArrowElement,
|
|
||||||
start: ValidLinearElement["start"],
|
|
||||||
end: ValidLinearElement["end"],
|
|
||||||
elementStore: ElementStore,
|
|
||||||
scene: Scene,
|
|
||||||
): {
|
|
||||||
linearElement: ExcalidrawLinearElement;
|
|
||||||
startBoundElement?: ExcalidrawElement;
|
|
||||||
endBoundElement?: ExcalidrawElement;
|
|
||||||
} => {
|
|
||||||
let startBoundElement;
|
|
||||||
let endBoundElement;
|
|
||||||
|
|
||||||
Object.assign(linearElement, {
|
|
||||||
startBinding: linearElement?.startBinding || null,
|
|
||||||
endBinding: linearElement.endBinding || null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (start) {
|
|
||||||
const width = start?.width ?? DEFAULT_DIMENSION;
|
|
||||||
const height = start?.height ?? DEFAULT_DIMENSION;
|
|
||||||
|
|
||||||
let existingElement;
|
|
||||||
if (start.id) {
|
|
||||||
existingElement = elementStore.getElement(start.id);
|
|
||||||
if (!existingElement) {
|
|
||||||
console.error(`No element for start binding with id ${start.id} found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startX = start.x || linearElement.x - width;
|
|
||||||
const startY = start.y || linearElement.y - height / 2;
|
|
||||||
const startType = existingElement ? existingElement.type : start.type;
|
|
||||||
|
|
||||||
if (startType) {
|
|
||||||
if (startType === "text") {
|
|
||||||
let text = "";
|
|
||||||
if (existingElement && existingElement.type === "text") {
|
|
||||||
text = existingElement.text;
|
|
||||||
} else if (start.type === "text") {
|
|
||||||
text = start.text;
|
|
||||||
}
|
|
||||||
if (!text) {
|
|
||||||
console.error(
|
|
||||||
`No text found for start binding text element for ${linearElement.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
startBoundElement = newTextElement({
|
|
||||||
x: startX,
|
|
||||||
y: startY,
|
|
||||||
type: "text",
|
|
||||||
...existingElement,
|
|
||||||
...start,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
// to position the text correctly when coordinates not provided
|
|
||||||
Object.assign(startBoundElement, {
|
|
||||||
x: start.x || linearElement.x - startBoundElement.width,
|
|
||||||
y: start.y || linearElement.y - startBoundElement.height / 2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (startType) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond": {
|
|
||||||
startBoundElement = newElement({
|
|
||||||
x: startX,
|
|
||||||
y: startY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...existingElement,
|
|
||||||
...start,
|
|
||||||
type: startType,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertNever(
|
|
||||||
linearElement as never,
|
|
||||||
`Unhandled element start type "${start.type}"`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindLinearElement(
|
|
||||||
linearElement,
|
|
||||||
startBoundElement as ExcalidrawBindableElement,
|
|
||||||
"start",
|
|
||||||
scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (end) {
|
|
||||||
const height = end?.height ?? DEFAULT_DIMENSION;
|
|
||||||
const width = end?.width ?? DEFAULT_DIMENSION;
|
|
||||||
|
|
||||||
let existingElement;
|
|
||||||
if (end.id) {
|
|
||||||
existingElement = elementStore.getElement(end.id);
|
|
||||||
if (!existingElement) {
|
|
||||||
console.error(`No element for end binding with id ${end.id} found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const endX = end.x || linearElement.x + linearElement.width;
|
|
||||||
const endY = end.y || linearElement.y - height / 2;
|
|
||||||
const endType = existingElement ? existingElement.type : end.type;
|
|
||||||
|
|
||||||
if (endType) {
|
|
||||||
if (endType === "text") {
|
|
||||||
let text = "";
|
|
||||||
if (existingElement && existingElement.type === "text") {
|
|
||||||
text = existingElement.text;
|
|
||||||
} else if (end.type === "text") {
|
|
||||||
text = end.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
console.error(
|
|
||||||
`No text found for end binding text element for ${linearElement.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
endBoundElement = newTextElement({
|
|
||||||
x: endX,
|
|
||||||
y: endY,
|
|
||||||
type: "text",
|
|
||||||
...existingElement,
|
|
||||||
...end,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
// to position the text correctly when coordinates not provided
|
|
||||||
Object.assign(endBoundElement, {
|
|
||||||
y: end.y || linearElement.y - endBoundElement.height / 2,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (endType) {
|
|
||||||
case "rectangle":
|
|
||||||
case "ellipse":
|
|
||||||
case "diamond": {
|
|
||||||
endBoundElement = newElement({
|
|
||||||
x: endX,
|
|
||||||
y: endY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...existingElement,
|
|
||||||
...end,
|
|
||||||
type: endType,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
assertNever(
|
|
||||||
linearElement as never,
|
|
||||||
`Unhandled element end type "${endType}"`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindLinearElement(
|
|
||||||
linearElement,
|
|
||||||
endBoundElement as ExcalidrawBindableElement,
|
|
||||||
"end",
|
|
||||||
scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safe check to early return for single point
|
|
||||||
if (linearElement.points.length < 2) {
|
|
||||||
return {
|
|
||||||
linearElement,
|
|
||||||
startBoundElement,
|
|
||||||
endBoundElement,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
|
||||||
const endPointIndex = linearElement.points.length - 1;
|
|
||||||
const delta = 0.5;
|
|
||||||
|
|
||||||
const newPoints = cloneJSON<readonly LocalPoint[]>(linearElement.points);
|
|
||||||
|
|
||||||
// left to right so shift the arrow towards right
|
|
||||||
if (
|
|
||||||
linearElement.points[endPointIndex][0] >
|
|
||||||
linearElement.points[endPointIndex - 1][0]
|
|
||||||
) {
|
|
||||||
newPoints[0][0] = delta;
|
|
||||||
newPoints[endPointIndex][0] -= delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// right to left so shift the arrow towards left
|
|
||||||
if (
|
|
||||||
linearElement.points[endPointIndex][0] <
|
|
||||||
linearElement.points[endPointIndex - 1][0]
|
|
||||||
) {
|
|
||||||
newPoints[0][0] = -delta;
|
|
||||||
newPoints[endPointIndex][0] += delta;
|
|
||||||
}
|
|
||||||
// top to bottom so shift the arrow towards top
|
|
||||||
if (
|
|
||||||
linearElement.points[endPointIndex][1] >
|
|
||||||
linearElement.points[endPointIndex - 1][1]
|
|
||||||
) {
|
|
||||||
newPoints[0][1] = delta;
|
|
||||||
newPoints[endPointIndex][1] -= delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bottom to top so shift the arrow towards bottom
|
|
||||||
if (
|
|
||||||
linearElement.points[endPointIndex][1] <
|
|
||||||
linearElement.points[endPointIndex - 1][1]
|
|
||||||
) {
|
|
||||||
newPoints[0][1] = -delta;
|
|
||||||
newPoints[endPointIndex][1] += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(
|
|
||||||
linearElement,
|
|
||||||
LinearElementEditor.getNormalizeElementPointsAndCoords({
|
|
||||||
...linearElement,
|
|
||||||
points: newPoints,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
linearElement,
|
|
||||||
startBoundElement,
|
|
||||||
endBoundElement,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class ElementStore {
|
class ElementStore {
|
||||||
excalidrawElements = new Map<string, ExcalidrawElement>();
|
excalidrawElements = new Map<string, ExcalidrawElement>();
|
||||||
|
|
||||||
@ -505,9 +274,317 @@ class ElementStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createBoundElement = (
|
||||||
|
binding: ValidLinearElement["start"] | ValidLinearElement["end"],
|
||||||
|
linearElement: ExcalidrawArrowElement,
|
||||||
|
edge: "start" | "end",
|
||||||
|
elementStore: ElementStore,
|
||||||
|
): ExcalidrawElement | undefined => {
|
||||||
|
if (!binding) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = binding?.width ?? DEFAULT_DIMENSION;
|
||||||
|
const height = binding?.height ?? DEFAULT_DIMENSION;
|
||||||
|
|
||||||
|
let existingElement;
|
||||||
|
if (binding.id) {
|
||||||
|
existingElement = elementStore.getElement(binding.id);
|
||||||
|
if (!existingElement) {
|
||||||
|
console.error(
|
||||||
|
`No element for ${edge} binding with id ${binding.id} found`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x =
|
||||||
|
binding.x ||
|
||||||
|
(edge === "start"
|
||||||
|
? linearElement.x - width
|
||||||
|
: linearElement.x + linearElement.width);
|
||||||
|
const y = binding.y || linearElement.y - height / 2;
|
||||||
|
const elementType = existingElement ? existingElement.type : binding.type;
|
||||||
|
|
||||||
|
if (!elementType) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementType === "text") {
|
||||||
|
let text = "";
|
||||||
|
if (existingElement && existingElement.type === "text") {
|
||||||
|
text = existingElement.text;
|
||||||
|
} else if (binding.type === "text") {
|
||||||
|
text = binding.text;
|
||||||
|
}
|
||||||
|
if (!text) {
|
||||||
|
console.error(
|
||||||
|
`No text found for ${edge} binding text element for ${linearElement.id}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const textElement = newTextElement({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
type: "text",
|
||||||
|
...existingElement,
|
||||||
|
...binding,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
// to position the text correctly when coordinates not provided
|
||||||
|
Object.assign(textElement, {
|
||||||
|
x:
|
||||||
|
binding.x ||
|
||||||
|
(edge === "start" ? linearElement.x - textElement.width : x),
|
||||||
|
y: binding.y || linearElement.y - textElement.height / 2,
|
||||||
|
});
|
||||||
|
return textElement;
|
||||||
|
}
|
||||||
|
switch (elementType) {
|
||||||
|
case "rectangle":
|
||||||
|
case "ellipse":
|
||||||
|
case "diamond": {
|
||||||
|
return newElement({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...existingElement,
|
||||||
|
...binding,
|
||||||
|
type: elementType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(
|
||||||
|
elementType as never,
|
||||||
|
`Unhandled element ${edge} type "${elementType}"`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindLinearElementToElement = (
|
||||||
|
linearElement: ExcalidrawArrowElement,
|
||||||
|
start: ValidLinearElement["start"],
|
||||||
|
end: ValidLinearElement["end"],
|
||||||
|
elementStore: ElementStore,
|
||||||
|
scene: Scene,
|
||||||
|
): {
|
||||||
|
linearElement: ExcalidrawLinearElement;
|
||||||
|
startBoundElement?: ExcalidrawElement;
|
||||||
|
endBoundElement?: ExcalidrawElement;
|
||||||
|
} => {
|
||||||
|
let startBoundElement;
|
||||||
|
let endBoundElement;
|
||||||
|
|
||||||
|
Object.assign(linearElement, {
|
||||||
|
startBinding: linearElement?.startBinding || null,
|
||||||
|
endBinding: linearElement.endBinding || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
startBoundElement = createBoundElement(
|
||||||
|
start,
|
||||||
|
linearElement,
|
||||||
|
"start",
|
||||||
|
elementStore,
|
||||||
|
);
|
||||||
|
if (startBoundElement) {
|
||||||
|
elementStore.add(startBoundElement);
|
||||||
|
scene.replaceAllElements(elementStore.getElementsMap());
|
||||||
|
bindLinearElement(
|
||||||
|
linearElement,
|
||||||
|
startBoundElement as ExcalidrawBindableElement,
|
||||||
|
"start",
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end) {
|
||||||
|
endBoundElement = createBoundElement(
|
||||||
|
end,
|
||||||
|
linearElement,
|
||||||
|
"end",
|
||||||
|
elementStore,
|
||||||
|
);
|
||||||
|
if (endBoundElement) {
|
||||||
|
elementStore.add(endBoundElement);
|
||||||
|
scene.replaceAllElements(elementStore.getElementsMap());
|
||||||
|
bindLinearElement(
|
||||||
|
linearElement,
|
||||||
|
endBoundElement as ExcalidrawBindableElement,
|
||||||
|
"end",
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linearElement.points.length < 2) {
|
||||||
|
return {
|
||||||
|
linearElement,
|
||||||
|
startBoundElement,
|
||||||
|
endBoundElement,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
||||||
|
if (!isElbowArrow(linearElement)) {
|
||||||
|
const endPointIndex = linearElement.points.length - 1;
|
||||||
|
const delta = 0.5;
|
||||||
|
|
||||||
|
const newPoints = cloneJSON<readonly LocalPoint[]>(linearElement.points);
|
||||||
|
|
||||||
|
// left to right so shift the arrow towards right
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] >
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = delta;
|
||||||
|
newPoints[endPointIndex][0] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// right to left so shift the arrow towards left
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][0] <
|
||||||
|
linearElement.points[endPointIndex - 1][0]
|
||||||
|
) {
|
||||||
|
newPoints[0][0] = -delta;
|
||||||
|
newPoints[endPointIndex][0] += delta;
|
||||||
|
}
|
||||||
|
// top to bottom so shift the arrow towards top
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] >
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = delta;
|
||||||
|
newPoints[endPointIndex][1] -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom to top so shift the arrow towards bottom
|
||||||
|
if (
|
||||||
|
linearElement.points[endPointIndex][1] <
|
||||||
|
linearElement.points[endPointIndex - 1][1]
|
||||||
|
) {
|
||||||
|
newPoints[0][1] = -delta;
|
||||||
|
newPoints[endPointIndex][1] += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
linearElement,
|
||||||
|
LinearElementEditor.getNormalizeElementPointsAndCoords({
|
||||||
|
...linearElement,
|
||||||
|
points: newPoints,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
linearElement,
|
||||||
|
startBoundElement,
|
||||||
|
endBoundElement,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustElbowArrowPoints = (elements: ExcalidrawElement[]) => {
|
||||||
|
const elementsMap = arrayToMap(elements) as NonDeletedSceneElementsMap;
|
||||||
|
const scene = new Scene(elementsMap);
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
if (isElbowArrow(element) && (element.startBinding || element.endBinding)) {
|
||||||
|
if (element.endBinding && element.endBinding.elementId) {
|
||||||
|
const midPoint = getBindingSideMidPoint(
|
||||||
|
element.endBinding,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const endBindableElement = elementsMap.get(
|
||||||
|
element.endBinding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
|
||||||
|
if (midPoint) {
|
||||||
|
LinearElementEditor.movePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
element.points.length - 1,
|
||||||
|
{
|
||||||
|
point: pointFrom(
|
||||||
|
midPoint[0] - element.x,
|
||||||
|
midPoint[1] - element.y,
|
||||||
|
),
|
||||||
|
isDragging: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFixedPoint = calculateFixedPointForElbowArrowBinding(
|
||||||
|
element,
|
||||||
|
endBindableElement,
|
||||||
|
"end",
|
||||||
|
elementsMap,
|
||||||
|
).fixedPoint;
|
||||||
|
|
||||||
|
if (newFixedPoint) {
|
||||||
|
Object.assign(element.endBinding, {
|
||||||
|
fixedPoint: newFixedPoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.startBinding && element.startBinding.elementId) {
|
||||||
|
const midPoint = getBindingSideMidPoint(
|
||||||
|
element.startBinding,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startBindableElement = elementsMap.get(
|
||||||
|
element.startBinding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
|
||||||
|
if (midPoint) {
|
||||||
|
LinearElementEditor.movePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
point: pointFrom(
|
||||||
|
midPoint[0] - element.x,
|
||||||
|
midPoint[1] - element.y,
|
||||||
|
),
|
||||||
|
isDragging: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFixedPoint = calculateFixedPointForElbowArrowBinding(
|
||||||
|
element,
|
||||||
|
startBindableElement,
|
||||||
|
"start",
|
||||||
|
elementsMap,
|
||||||
|
).fixedPoint;
|
||||||
|
if (newFixedPoint) {
|
||||||
|
Object.assign(element.startBinding, {
|
||||||
|
fixedPoint: newFixedPoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const convertToExcalidrawElements = (
|
export const convertToExcalidrawElements = (
|
||||||
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
||||||
opts?: { regenerateIds: boolean },
|
opts?: { regenerateIds: boolean; useElbow?: boolean },
|
||||||
) => {
|
) => {
|
||||||
if (!elementsSkeleton) {
|
if (!elementsSkeleton) {
|
||||||
return [];
|
return [];
|
||||||
@ -560,19 +637,32 @@ export const convertToExcalidrawElements = (
|
|||||||
case "arrow": {
|
case "arrow": {
|
||||||
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
||||||
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
||||||
excalidrawElement = newArrowElement({
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
endArrowhead: "arrow",
|
|
||||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
|
||||||
...element,
|
|
||||||
type: "arrow",
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(
|
if (!opts?.useElbow) {
|
||||||
excalidrawElement,
|
excalidrawElement = newArrowElement({
|
||||||
getSizeFromPoints(excalidrawElement.points),
|
width,
|
||||||
);
|
height,
|
||||||
|
endArrowhead: "arrow",
|
||||||
|
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||||
|
...element,
|
||||||
|
type: "arrow",
|
||||||
|
elbowed: opts?.useElbow,
|
||||||
|
});
|
||||||
|
Object.assign(
|
||||||
|
excalidrawElement,
|
||||||
|
getSizeFromPoints(excalidrawElement.points),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
excalidrawElement = newArrowElement({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
endArrowhead: "arrow",
|
||||||
|
...element,
|
||||||
|
type: "arrow",
|
||||||
|
elbowed: opts?.useElbow,
|
||||||
|
roundness: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@ -804,5 +894,12 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return elementStore.getElements();
|
const finalElements = elementStore.getElements();
|
||||||
|
|
||||||
|
// Adjust elbow arrow points now that all elements are in the scene
|
||||||
|
if (opts?.useElbow) {
|
||||||
|
adjustElbowArrowPoints(finalElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalElements;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,7 @@ exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active too
|
|||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
C -->|Two| E[iPhone]
|
||||||
C -->|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="89" height="158" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
|
C -->|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center; justify-content: space-between;"><div class="ttd-dialog-panel-button-container-left"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="89" height="158" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center; justify-content: space-between;"><div class="ttd-dialog-panel-button-container-left"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div><div class="dialog-mermaid-arrow-type"><div class="RadioGroup"><div class="RadioGroup__choice active" title="Curved arrow"><input aria-label="Curved arrow" type="radio" checked="" name="mermaid arrow config"><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" class="" fill="none" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g><path d="M16,12L20,9L16,6"></path><path d="M6 20c0 -6.075 4.925 -11 11 -11h3"></path></g></svg></div><div class="RadioGroup__choice" title="Elbow arrow"><input aria-label="Elbow arrow" type="radio" name="mermaid arrow config"><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" class="" fill="none" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4,19L10,19C11.097,19 12,18.097 12,17L12,9C12,7.903 12.903,7 14,7L21,7"></path><path d="M18 4l3 3l-3 3"></path></g></svg></div></div></div></div></div></div></div></div></div></div></div></div>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `
|
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user