feat: add support for more UML arrowheads (#7391)
This commit is contained in:
parent
a04cc707c3
commit
b9cfbc2077
@ -15,7 +15,7 @@ import { IconPicker } from "../components/IconPicker";
|
|||||||
import {
|
import {
|
||||||
ArrowheadArrowIcon,
|
ArrowheadArrowIcon,
|
||||||
ArrowheadBarIcon,
|
ArrowheadBarIcon,
|
||||||
ArrowheadDotIcon,
|
ArrowheadCircleIcon,
|
||||||
ArrowheadTriangleIcon,
|
ArrowheadTriangleIcon,
|
||||||
ArrowheadNoneIcon,
|
ArrowheadNoneIcon,
|
||||||
StrokeStyleDashedIcon,
|
StrokeStyleDashedIcon,
|
||||||
@ -45,6 +45,10 @@ import {
|
|||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
FillZigZagIcon,
|
FillZigZagIcon,
|
||||||
|
ArrowheadTriangleOutlineIcon,
|
||||||
|
ArrowheadCircleOutlineIcon,
|
||||||
|
ArrowheadDiamondIcon,
|
||||||
|
ArrowheadDiamondOutlineIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
@ -1013,6 +1017,77 @@ export const actionChangeRoundness = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getArrowheadOptions = (flip: boolean) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: null,
|
||||||
|
text: t("labels.arrowhead_none"),
|
||||||
|
keyBinding: "q",
|
||||||
|
icon: ArrowheadNoneIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "arrow",
|
||||||
|
text: t("labels.arrowhead_arrow"),
|
||||||
|
keyBinding: "w",
|
||||||
|
icon: <ArrowheadArrowIcon flip={flip} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bar",
|
||||||
|
text: t("labels.arrowhead_bar"),
|
||||||
|
keyBinding: "e",
|
||||||
|
icon: <ArrowheadBarIcon flip={flip} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "dot",
|
||||||
|
text: t("labels.arrowhead_circle"),
|
||||||
|
keyBinding: null,
|
||||||
|
icon: <ArrowheadCircleIcon flip={flip} />,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "circle",
|
||||||
|
text: t("labels.arrowhead_circle"),
|
||||||
|
keyBinding: "r",
|
||||||
|
icon: <ArrowheadCircleIcon flip={flip} />,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "circle_outline",
|
||||||
|
text: t("labels.arrowhead_circle_outline"),
|
||||||
|
keyBinding: null,
|
||||||
|
icon: <ArrowheadCircleOutlineIcon flip={flip} />,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "triangle",
|
||||||
|
text: t("labels.arrowhead_triangle"),
|
||||||
|
icon: <ArrowheadTriangleIcon flip={flip} />,
|
||||||
|
keyBinding: "t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "triangle_outline",
|
||||||
|
text: t("labels.arrowhead_triangle_outline"),
|
||||||
|
icon: <ArrowheadTriangleOutlineIcon flip={flip} />,
|
||||||
|
keyBinding: null,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "diamond",
|
||||||
|
text: t("labels.arrowhead_diamond"),
|
||||||
|
icon: <ArrowheadDiamondIcon flip={flip} />,
|
||||||
|
keyBinding: null,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "diamond_outline",
|
||||||
|
text: t("labels.arrowhead_diamond_outline"),
|
||||||
|
icon: <ArrowheadDiamondOutlineIcon flip={flip} />,
|
||||||
|
keyBinding: null,
|
||||||
|
showInPicker: false,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
};
|
||||||
|
|
||||||
export const actionChangeArrowhead = register({
|
export const actionChangeArrowhead = register({
|
||||||
name: "changeArrowhead",
|
name: "changeArrowhead",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -1059,38 +1134,7 @@ export const actionChangeArrowhead = register({
|
|||||||
<div className="iconSelectList buttonList">
|
<div className="iconSelectList buttonList">
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="arrowhead_start"
|
label="arrowhead_start"
|
||||||
options={[
|
options={getArrowheadOptions(!isRTL)}
|
||||||
{
|
|
||||||
value: null,
|
|
||||||
text: t("labels.arrowhead_none"),
|
|
||||||
icon: ArrowheadNoneIcon,
|
|
||||||
keyBinding: "q",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "arrow",
|
|
||||||
text: t("labels.arrowhead_arrow"),
|
|
||||||
icon: <ArrowheadArrowIcon flip={!isRTL} />,
|
|
||||||
keyBinding: "w",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "bar",
|
|
||||||
text: t("labels.arrowhead_bar"),
|
|
||||||
icon: <ArrowheadBarIcon flip={!isRTL} />,
|
|
||||||
keyBinding: "e",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "dot",
|
|
||||||
text: t("labels.arrowhead_dot"),
|
|
||||||
icon: <ArrowheadDotIcon flip={!isRTL} />,
|
|
||||||
keyBinding: "r",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "triangle",
|
|
||||||
text: t("labels.arrowhead_triangle"),
|
|
||||||
icon: <ArrowheadTriangleIcon flip={!isRTL} />,
|
|
||||||
keyBinding: "t",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={getFormValue<Arrowhead | null>(
|
value={getFormValue<Arrowhead | null>(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
@ -1106,38 +1150,7 @@ export const actionChangeArrowhead = register({
|
|||||||
<IconPicker
|
<IconPicker
|
||||||
label="arrowhead_end"
|
label="arrowhead_end"
|
||||||
group="arrowheads"
|
group="arrowheads"
|
||||||
options={[
|
options={getArrowheadOptions(!!isRTL)}
|
||||||
{
|
|
||||||
value: null,
|
|
||||||
text: t("labels.arrowhead_none"),
|
|
||||||
keyBinding: "q",
|
|
||||||
icon: ArrowheadNoneIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "arrow",
|
|
||||||
text: t("labels.arrowhead_arrow"),
|
|
||||||
keyBinding: "w",
|
|
||||||
icon: <ArrowheadArrowIcon flip={isRTL} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "bar",
|
|
||||||
text: t("labels.arrowhead_bar"),
|
|
||||||
keyBinding: "e",
|
|
||||||
icon: <ArrowheadBarIcon flip={isRTL} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "dot",
|
|
||||||
text: t("labels.arrowhead_dot"),
|
|
||||||
keyBinding: "r",
|
|
||||||
icon: <ArrowheadDotIcon flip={isRTL} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "triangle",
|
|
||||||
text: t("labels.arrowhead_triangle"),
|
|
||||||
icon: <ArrowheadTriangleIcon flip={isRTL} />,
|
|
||||||
keyBinding: "t",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={getFormValue<Arrowhead | null>(
|
value={getFormValue<Arrowhead | null>(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
|
@ -1556,6 +1556,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
imageCache: this.imageCache,
|
imageCache: this.imageCache,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
renderGrid: true,
|
renderGrid: true,
|
||||||
|
canvasBackgroundColor:
|
||||||
|
this.state.viewBackgroundColor,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<InteractiveCanvas
|
<InteractiveCanvas
|
||||||
|
@ -15,7 +15,12 @@ function Picker<T>({
|
|||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: T;
|
||||||
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
options: {
|
||||||
|
value: T;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
keyBinding: string | null;
|
||||||
|
}[];
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
@ -110,9 +115,11 @@ function Picker<T>({
|
|||||||
(event.currentTarget as HTMLButtonElement).focus();
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
onChange(option.value);
|
onChange(option.value);
|
||||||
}}
|
}}
|
||||||
title={`${option.text} — ${option.keyBinding.toUpperCase()}`}
|
title={`${option.text} ${
|
||||||
|
option.keyBinding && `— ${option.keyBinding.toUpperCase()}`
|
||||||
|
}`}
|
||||||
aria-label={option.text || "none"}
|
aria-label={option.text || "none"}
|
||||||
aria-keyshortcuts={option.keyBinding}
|
aria-keyshortcuts={option.keyBinding || undefined}
|
||||||
key={option.text}
|
key={option.text}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (el && i === 0) {
|
if (el && i === 0) {
|
||||||
@ -127,7 +134,9 @@ function Picker<T>({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.icon}
|
{option.icon}
|
||||||
|
{option.keyBinding && (
|
||||||
<span className="picker-keybinding">{option.keyBinding}</span>
|
<span className="picker-keybinding">{option.keyBinding}</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -144,7 +153,13 @@ export function IconPicker<T>({
|
|||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: T;
|
||||||
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
options: readonly {
|
||||||
|
value: T;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
keyBinding: string | null;
|
||||||
|
showInPicker?: boolean;
|
||||||
|
}[];
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
group?: string;
|
group?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -173,7 +188,7 @@ export function IconPicker<T>({
|
|||||||
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
|
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
|
||||||
>
|
>
|
||||||
<Picker
|
<Picker
|
||||||
options={options}
|
options={options.filter((opt) => opt.showInPicker !== false)}
|
||||||
value={value}
|
value={value}
|
||||||
label={label}
|
label={label}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -1281,7 +1281,7 @@ export const ArrowheadArrowIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadDotIcon = React.memo(
|
export const ArrowheadCircleIcon = React.memo(
|
||||||
({ flip = false }: { flip?: boolean }) =>
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g
|
<g
|
||||||
@ -1296,6 +1296,22 @@ export const ArrowheadDotIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ArrowheadCircleOutlineIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M26 10L6 10" />
|
||||||
|
<circle r="4" transform="matrix(-1 0 0 1 30 10)" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const ArrowheadBarIcon = React.memo(
|
export const ArrowheadBarIcon = React.memo(
|
||||||
({ flip = false }: { flip?: boolean }) =>
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
@ -1326,6 +1342,58 @@ export const ArrowheadTriangleIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ArrowheadTriangleOutlineIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M6,9.5H27" />
|
||||||
|
<path d="M27,5L34,10L27,14Z" fill="none" />
|
||||||
|
</g>,
|
||||||
|
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadDiamondIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M6,9.5H20" />
|
||||||
|
<path d="M27,5L34,10L27,14L20,9.5Z" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ArrowheadDiamondOutlineIcon = React.memo(
|
||||||
|
({ flip = false }: { flip?: boolean }) =>
|
||||||
|
createIcon(
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path d="M6,9.5H20" />
|
||||||
|
<path d="M27,5L34,10L27,14L20,9.5Z" />
|
||||||
|
</g>,
|
||||||
|
{ width: 40, height: 20 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const FontSizeSmallIcon = createIcon(
|
export const FontSizeSmallIcon = createIcon(
|
||||||
<>
|
<>
|
||||||
<g clipPath="url(#a)">
|
<g clipPath="url(#a)">
|
||||||
|
@ -484,6 +484,31 @@ const getFreeDrawElementAbsoluteCoords = (
|
|||||||
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @returns number in pixels */
|
||||||
|
export const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
||||||
|
switch (arrowhead) {
|
||||||
|
case "arrow":
|
||||||
|
return 25;
|
||||||
|
case "diamond":
|
||||||
|
case "diamond_outline":
|
||||||
|
return 12;
|
||||||
|
default:
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns number in degrees */
|
||||||
|
export const getArrowheadAngle = (arrowhead: Arrowhead): number => {
|
||||||
|
switch (arrowhead) {
|
||||||
|
case "bar":
|
||||||
|
return 90;
|
||||||
|
case "arrow":
|
||||||
|
return 20;
|
||||||
|
default:
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getArrowheadPoints = (
|
export const getArrowheadPoints = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
shape: Drawable[],
|
shape: Drawable[],
|
||||||
@ -536,53 +561,82 @@ export const getArrowheadPoints = (
|
|||||||
const nx = (x2 - x1) / distance;
|
const nx = (x2 - x1) / distance;
|
||||||
const ny = (y2 - y1) / distance;
|
const ny = (y2 - y1) / distance;
|
||||||
|
|
||||||
const size = {
|
const size = getArrowheadSize(arrowhead);
|
||||||
arrow: 30,
|
|
||||||
bar: 15,
|
|
||||||
dot: 15,
|
|
||||||
triangle: 15,
|
|
||||||
}[arrowhead]; // pixels (will differ for each arrowhead)
|
|
||||||
|
|
||||||
let length = 0;
|
let length = 0;
|
||||||
|
|
||||||
if (arrowhead === "arrow") {
|
{
|
||||||
// Length for -> arrows is based on the length of the last section
|
// Length for -> arrows is based on the length of the last section
|
||||||
const [cx, cy] = element.points[element.points.length - 1];
|
const [cx, cy] =
|
||||||
|
position === "end"
|
||||||
|
? element.points[element.points.length - 1]
|
||||||
|
: element.points[0];
|
||||||
|
const [px, py] =
|
||||||
|
element.points.length > 1
|
||||||
|
? position === "end"
|
||||||
|
? element.points[element.points.length - 2]
|
||||||
|
: element.points[1]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
length = Math.hypot(cx - px, cy - py);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
||||||
|
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
||||||
|
const lengthMultiplier =
|
||||||
|
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
|
||||||
|
const minSize = Math.min(size, length * lengthMultiplier);
|
||||||
|
const xs = x2 - nx * minSize;
|
||||||
|
const ys = y2 - ny * minSize;
|
||||||
|
|
||||||
|
if (
|
||||||
|
arrowhead === "dot" ||
|
||||||
|
arrowhead === "circle" ||
|
||||||
|
arrowhead === "circle_outline"
|
||||||
|
) {
|
||||||
|
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
|
||||||
|
return [x2, y2, diameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = getArrowheadAngle(arrowhead);
|
||||||
|
|
||||||
|
// Return points
|
||||||
|
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
||||||
|
const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
|
||||||
|
|
||||||
|
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
||||||
|
// point opposite to the arrowhead point
|
||||||
|
let ox;
|
||||||
|
let oy;
|
||||||
|
|
||||||
|
if (position === "start") {
|
||||||
|
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||||
|
|
||||||
|
[ox, oy] = rotate(
|
||||||
|
x2 + minSize * 2,
|
||||||
|
y2,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
Math.atan2(py - y2, px - x2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const [px, py] =
|
const [px, py] =
|
||||||
element.points.length > 1
|
element.points.length > 1
|
||||||
? element.points[element.points.length - 2]
|
? element.points[element.points.length - 2]
|
||||||
: [0, 0];
|
: [0, 0];
|
||||||
|
|
||||||
length = Math.hypot(cx - px, cy - py);
|
[ox, oy] = rotate(
|
||||||
} else {
|
x2 - minSize * 2,
|
||||||
// Length for other arrowhead types is based on the total length of the line
|
y2,
|
||||||
for (let i = 0; i < element.points.length; i++) {
|
x2,
|
||||||
const [px, py] = element.points[i - 1] || [0, 0];
|
y2,
|
||||||
const [cx, cy] = element.points[i];
|
Math.atan2(y2 - py, x2 - px),
|
||||||
length += Math.hypot(cx - px, cy - py);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
return [x2, y2, x3, y3, ox, oy, x4, y4];
|
||||||
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
|
||||||
const minSize = Math.min(size, length / 2);
|
|
||||||
const xs = x2 - nx * minSize;
|
|
||||||
const ys = y2 - ny * minSize;
|
|
||||||
|
|
||||||
if (arrowhead === "dot") {
|
|
||||||
const r = Math.hypot(ys - y2, xs - x2) + element.strokeWidth;
|
|
||||||
return [x2, y2, r];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const angle = {
|
|
||||||
arrow: 20,
|
|
||||||
bar: 90,
|
|
||||||
triangle: 25,
|
|
||||||
}[arrowhead]; // degrees
|
|
||||||
|
|
||||||
// Return points
|
|
||||||
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
|
||||||
const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
|
|
||||||
return [x2, y2, x3, y3, x4, y4];
|
return [x2, y2, x3, y3, x4, y4];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1444,7 +1444,7 @@ export class LinearElementEditor {
|
|||||||
x2 = maxX + element.x;
|
x2 = maxX + element.x;
|
||||||
y2 = maxY + element.y;
|
y2 = maxY + element.y;
|
||||||
} else {
|
} else {
|
||||||
const shape = ShapeCache.generateElementShape(element);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
|
|
||||||
// first element is always the curve
|
// first element is always the curve
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
@ -223,7 +223,16 @@ export type PointBinding = {
|
|||||||
gap: number;
|
gap: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Arrowhead = "arrow" | "bar" | "dot" | "triangle";
|
export type Arrowhead =
|
||||||
|
| "arrow"
|
||||||
|
| "bar"
|
||||||
|
| "dot" // legacy. Do not use for new elements.
|
||||||
|
| "circle"
|
||||||
|
| "circle_outline"
|
||||||
|
| "triangle"
|
||||||
|
| "triangle_outline"
|
||||||
|
| "diamond"
|
||||||
|
| "diamond_outline";
|
||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
|
@ -38,8 +38,12 @@
|
|||||||
"arrowhead_none": "None",
|
"arrowhead_none": "None",
|
||||||
"arrowhead_arrow": "Arrow",
|
"arrowhead_arrow": "Arrow",
|
||||||
"arrowhead_bar": "Bar",
|
"arrowhead_bar": "Bar",
|
||||||
"arrowhead_dot": "Dot",
|
"arrowhead_circle": "Circle",
|
||||||
|
"arrowhead_circle_outline": "Circle (outline)",
|
||||||
"arrowhead_triangle": "Triangle",
|
"arrowhead_triangle": "Triangle",
|
||||||
|
"arrowhead_triangle_outline": "Triangle (outline)",
|
||||||
|
"arrowhead_diamond": "Diamond",
|
||||||
|
"arrowhead_diamond_outline": "Diamond (outline)",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||||
|
16
src/math.ts
16
src/math.ts
@ -15,18 +15,20 @@ import { Mutable } from "./utility-types";
|
|||||||
import { ShapeCache } from "./scene/ShapeCache";
|
import { ShapeCache } from "./scene/ShapeCache";
|
||||||
|
|
||||||
export const rotate = (
|
export const rotate = (
|
||||||
x1: number,
|
// target point to rotate
|
||||||
y1: number,
|
x: number,
|
||||||
x2: number,
|
y: number,
|
||||||
y2: number,
|
// point to rotate against
|
||||||
|
cx: number,
|
||||||
|
cy: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
): [number, number] =>
|
): [number, number] =>
|
||||||
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||||
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||||
[
|
[
|
||||||
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
|
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||||
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
|
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const rotatePoint = (
|
export const rotatePoint = (
|
||||||
@ -303,7 +305,7 @@ export const getControlPointsForBezierCurve = (
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
endPoint: Point,
|
endPoint: Point,
|
||||||
) => {
|
) => {
|
||||||
const shape = ShapeCache.generateElementShape(element);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||||||
import type { Drawable } from "roughjs/bin/core";
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
import type { RoughSVG } from "roughjs/bin/svg";
|
import type { RoughSVG } from "roughjs/bin/svg";
|
||||||
|
|
||||||
import { StaticCanvasRenderConfig } from "../scene/types";
|
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
|
||||||
import {
|
import {
|
||||||
distance,
|
distance,
|
||||||
getFontString,
|
getFontString,
|
||||||
@ -638,7 +638,7 @@ export const renderElement = (
|
|||||||
// TODO investigate if we can do this in situ. Right now we need to call
|
// TODO investigate if we can do this in situ. Right now we need to call
|
||||||
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
||||||
// rely on existing shapes
|
// rely on existing shapes
|
||||||
ShapeCache.generateElementShape(element);
|
ShapeCache.generateElementShape(element, null);
|
||||||
|
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
@ -680,7 +680,7 @@ export const renderElement = (
|
|||||||
// TODO investigate if we can do this in situ. Right now we need to call
|
// TODO investigate if we can do this in situ. Right now we need to call
|
||||||
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
||||||
// rely on existing shapes
|
// rely on existing shapes
|
||||||
ShapeCache.generateElementShape(element, renderConfig.isExporting);
|
ShapeCache.generateElementShape(element, renderConfig);
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + appState.scrollX;
|
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||||
@ -876,11 +876,7 @@ export const renderElementToSvg = (
|
|||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
renderConfig: {
|
renderConfig: SVGRenderConfig,
|
||||||
exportWithDarkMode: boolean;
|
|
||||||
renderEmbeddables: boolean;
|
|
||||||
frameRendering: AppState["frameRendering"];
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
const offset = { x: offsetX, y: offsetY };
|
const offset = { x: offsetX, y: offsetY };
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
@ -933,7 +929,7 @@ export const renderElementToSvg = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
const shape = ShapeCache.generateElementShape(element);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@ -964,7 +960,7 @@ export const renderElementToSvg = (
|
|||||||
case "iframe":
|
case "iframe":
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
// render placeholder rectangle
|
// render placeholder rectangle
|
||||||
const shape = ShapeCache.generateElementShape(element, true);
|
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@ -1113,7 +1109,7 @@ export const renderElementToSvg = (
|
|||||||
}
|
}
|
||||||
group.setAttribute("stroke-linecap", "round");
|
group.setAttribute("stroke-linecap", "round");
|
||||||
|
|
||||||
const shapes = ShapeCache.generateElementShape(element);
|
const shapes = ShapeCache.generateElementShape(element, renderConfig);
|
||||||
shapes.forEach((shape) => {
|
shapes.forEach((shape) => {
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
@ -1156,7 +1152,10 @@ export const renderElementToSvg = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
const backgroundFillShape = ShapeCache.generateElementShape(element);
|
const backgroundFillShape = ShapeCache.generateElementShape(
|
||||||
|
element,
|
||||||
|
renderConfig,
|
||||||
|
);
|
||||||
const node = backgroundFillShape
|
const node = backgroundFillShape
|
||||||
? roughSVGDrawWithPrecision(
|
? roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
|
@ -30,6 +30,7 @@ import { roundRect } from "./roundRect";
|
|||||||
import {
|
import {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
|
SVGRenderConfig,
|
||||||
StaticCanvasRenderConfig,
|
StaticCanvasRenderConfig,
|
||||||
StaticSceneRenderConfig,
|
StaticSceneRenderConfig,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
@ -1448,29 +1449,12 @@ export const renderSceneToSvg = (
|
|||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
svgRoot: SVGElement,
|
svgRoot: SVGElement,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
{
|
renderConfig: SVGRenderConfig,
|
||||||
offsetX = 0,
|
|
||||||
offsetY = 0,
|
|
||||||
exportWithDarkMode,
|
|
||||||
renderEmbeddables,
|
|
||||||
frameRendering,
|
|
||||||
}: {
|
|
||||||
offsetX?: number;
|
|
||||||
offsetY?: number;
|
|
||||||
exportWithDarkMode: boolean;
|
|
||||||
renderEmbeddables: boolean;
|
|
||||||
frameRendering: AppState["frameRendering"];
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
if (!svgRoot) {
|
if (!svgRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderConfig = {
|
|
||||||
exportWithDarkMode,
|
|
||||||
renderEmbeddables,
|
|
||||||
frameRendering,
|
|
||||||
};
|
|
||||||
// render elements
|
// render elements
|
||||||
elements
|
elements
|
||||||
.filter((el) => !isIframeLikeOrItsLabel(el))
|
.filter((el) => !isIframeLikeOrItsLabel(el))
|
||||||
@ -1482,8 +1466,8 @@ export const renderSceneToSvg = (
|
|||||||
rsvg,
|
rsvg,
|
||||||
svgRoot,
|
svgRoot,
|
||||||
files,
|
files,
|
||||||
element.x + offsetX,
|
element.x + renderConfig.offsetX,
|
||||||
element.y + offsetY,
|
element.y + renderConfig.offsetY,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -1503,8 +1487,8 @@ export const renderSceneToSvg = (
|
|||||||
rsvg,
|
rsvg,
|
||||||
svgRoot,
|
svgRoot,
|
||||||
files,
|
files,
|
||||||
element.x + offsetX,
|
element.x + renderConfig.offsetX,
|
||||||
element.y + offsetY,
|
element.y + renderConfig.offsetY,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
@ -145,6 +145,126 @@ const modifyIframeLikeForRoughOptions = (
|
|||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getArrowheadShapes = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
shape: Drawable[],
|
||||||
|
position: "start" | "end",
|
||||||
|
arrowhead: Arrowhead,
|
||||||
|
generator: RoughGenerator,
|
||||||
|
options: Options,
|
||||||
|
canvasBackgroundColor: string,
|
||||||
|
) => {
|
||||||
|
const arrowheadPoints = getArrowheadPoints(
|
||||||
|
element,
|
||||||
|
shape,
|
||||||
|
position,
|
||||||
|
arrowhead,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (arrowheadPoints === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (arrowhead) {
|
||||||
|
case "dot":
|
||||||
|
case "circle":
|
||||||
|
case "circle_outline": {
|
||||||
|
const [x, y, diameter] = arrowheadPoints;
|
||||||
|
|
||||||
|
// always use solid stroke for arrowhead
|
||||||
|
delete options.strokeLineDash;
|
||||||
|
|
||||||
|
return [
|
||||||
|
generator.circle(x, y, diameter, {
|
||||||
|
...options,
|
||||||
|
fill:
|
||||||
|
arrowhead === "circle_outline"
|
||||||
|
? canvasBackgroundColor
|
||||||
|
: element.strokeColor,
|
||||||
|
|
||||||
|
fillStyle: "solid",
|
||||||
|
stroke: element.strokeColor,
|
||||||
|
roughness: Math.min(0.5, options.roughness || 0),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "triangle":
|
||||||
|
case "triangle_outline": {
|
||||||
|
const [x, y, x2, y2, x3, y3] = arrowheadPoints;
|
||||||
|
|
||||||
|
// always use solid stroke for arrowhead
|
||||||
|
delete options.strokeLineDash;
|
||||||
|
|
||||||
|
return [
|
||||||
|
generator.polygon(
|
||||||
|
[
|
||||||
|
[x, y],
|
||||||
|
[x2, y2],
|
||||||
|
[x3, y3],
|
||||||
|
[x, y],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
fill:
|
||||||
|
arrowhead === "triangle_outline"
|
||||||
|
? canvasBackgroundColor
|
||||||
|
: element.strokeColor,
|
||||||
|
fillStyle: "solid",
|
||||||
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "diamond":
|
||||||
|
case "diamond_outline": {
|
||||||
|
const [x, y, x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
|
||||||
|
// always use solid stroke for arrowhead
|
||||||
|
delete options.strokeLineDash;
|
||||||
|
|
||||||
|
return [
|
||||||
|
generator.polygon(
|
||||||
|
[
|
||||||
|
[x, y],
|
||||||
|
[x2, y2],
|
||||||
|
[x3, y3],
|
||||||
|
[x4, y4],
|
||||||
|
[x, y],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
fill:
|
||||||
|
arrowhead === "diamond_outline"
|
||||||
|
? canvasBackgroundColor
|
||||||
|
: element.strokeColor,
|
||||||
|
fillStyle: "solid",
|
||||||
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "bar":
|
||||||
|
case "arrow":
|
||||||
|
default: {
|
||||||
|
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
|
||||||
|
if (element.strokeStyle === "dotted") {
|
||||||
|
// for dotted arrows caps, reduce gap to make it more legible
|
||||||
|
const dash = getDashArrayDotted(element.strokeWidth - 1);
|
||||||
|
options.strokeLineDash = [dash[0], dash[1] - 1];
|
||||||
|
} else {
|
||||||
|
// for solid/dashed, keep solid arrow cap
|
||||||
|
delete options.strokeLineDash;
|
||||||
|
}
|
||||||
|
options.roughness = Math.min(1, options.roughness || 0);
|
||||||
|
return [
|
||||||
|
generator.line(x3, y3, x2, y2, options),
|
||||||
|
generator.line(x4, y4, x2, y2, options),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the roughjs shape for given element.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
@ -155,7 +275,10 @@ const modifyIframeLikeForRoughOptions = (
|
|||||||
export const _generateElementShape = (
|
export const _generateElementShape = (
|
||||||
element: Exclude<NonDeletedExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<NonDeletedExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
isExporting: boolean = false,
|
{
|
||||||
|
isExporting,
|
||||||
|
canvasBackgroundColor,
|
||||||
|
}: { isExporting: boolean; canvasBackgroundColor: string },
|
||||||
): Drawable | Drawable[] | null => {
|
): Drawable | Drawable[] | null => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
@ -276,83 +399,15 @@ export const _generateElementShape = (
|
|||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
||||||
|
|
||||||
const getArrowheadShapes = (
|
|
||||||
element: ExcalidrawLinearElement,
|
|
||||||
shape: Drawable[],
|
|
||||||
position: "start" | "end",
|
|
||||||
arrowhead: Arrowhead,
|
|
||||||
) => {
|
|
||||||
const arrowheadPoints = getArrowheadPoints(
|
|
||||||
element,
|
|
||||||
shape,
|
|
||||||
position,
|
|
||||||
arrowhead,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (arrowheadPoints === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other arrowheads here...
|
|
||||||
if (arrowhead === "dot") {
|
|
||||||
const [x, y, r] = arrowheadPoints;
|
|
||||||
|
|
||||||
return [
|
|
||||||
generator.circle(x, y, r, {
|
|
||||||
...options,
|
|
||||||
fill: element.strokeColor,
|
|
||||||
fillStyle: "solid",
|
|
||||||
stroke: "none",
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arrowhead === "triangle") {
|
|
||||||
const [x, y, x2, y2, x3, y3] = arrowheadPoints;
|
|
||||||
|
|
||||||
// always use solid stroke for triangle arrowhead
|
|
||||||
delete options.strokeLineDash;
|
|
||||||
|
|
||||||
return [
|
|
||||||
generator.polygon(
|
|
||||||
[
|
|
||||||
[x, y],
|
|
||||||
[x2, y2],
|
|
||||||
[x3, y3],
|
|
||||||
[x, y],
|
|
||||||
],
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
fill: element.strokeColor,
|
|
||||||
fillStyle: "solid",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrow arrowheads
|
|
||||||
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
|
||||||
|
|
||||||
if (element.strokeStyle === "dotted") {
|
|
||||||
// for dotted arrows caps, reduce gap to make it more legible
|
|
||||||
const dash = getDashArrayDotted(element.strokeWidth - 1);
|
|
||||||
options.strokeLineDash = [dash[0], dash[1] - 1];
|
|
||||||
} else {
|
|
||||||
// for solid/dashed, keep solid arrow cap
|
|
||||||
delete options.strokeLineDash;
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
generator.line(x3, y3, x2, y2, options),
|
|
||||||
generator.line(x4, y4, x2, y2, options),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (startArrowhead !== null) {
|
if (startArrowhead !== null) {
|
||||||
const shapes = getArrowheadShapes(
|
const shapes = getArrowheadShapes(
|
||||||
element,
|
element,
|
||||||
shape,
|
shape,
|
||||||
"start",
|
"start",
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
|
generator,
|
||||||
|
options,
|
||||||
|
canvasBackgroundColor,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@ -367,6 +422,9 @@ export const _generateElementShape = (
|
|||||||
shape,
|
shape,
|
||||||
"end",
|
"end",
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
|
generator,
|
||||||
|
options,
|
||||||
|
canvasBackgroundColor,
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
import { elementWithCanvasCache } from "../renderer/renderElement";
|
import { elementWithCanvasCache } from "../renderer/renderElement";
|
||||||
import { _generateElementShape } from "./Shape";
|
import { _generateElementShape } from "./Shape";
|
||||||
import { ElementShape, ElementShapes } from "./types";
|
import { ElementShape, ElementShapes } from "./types";
|
||||||
|
import { COLOR_PALETTE } from "../colors";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
||||||
private static rg = new RoughGenerator();
|
private static rg = new RoughGenerator();
|
||||||
@ -46,10 +48,15 @@ export class ShapeCache {
|
|||||||
T extends Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
T extends Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
>(
|
>(
|
||||||
element: T,
|
element: T,
|
||||||
isExporting = false,
|
renderConfig: {
|
||||||
|
isExporting: boolean;
|
||||||
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
|
} | null,
|
||||||
) => {
|
) => {
|
||||||
// when exporting, always regenerated to guarantee the latest shape
|
// when exporting, always regenerated to guarantee the latest shape
|
||||||
const cachedShape = isExporting ? undefined : ShapeCache.get(element);
|
const cachedShape = renderConfig?.isExporting
|
||||||
|
? undefined
|
||||||
|
: ShapeCache.get(element);
|
||||||
|
|
||||||
// `null` indicates no rc shape applicable for this element type,
|
// `null` indicates no rc shape applicable for this element type,
|
||||||
// but it's considered a valid cache value (= do not regenerate)
|
// but it's considered a valid cache value (= do not regenerate)
|
||||||
@ -62,7 +69,10 @@ export class ShapeCache {
|
|||||||
const shape = _generateElementShape(
|
const shape = _generateElementShape(
|
||||||
element,
|
element,
|
||||||
ShapeCache.rg,
|
ShapeCache.rg,
|
||||||
isExporting,
|
renderConfig || {
|
||||||
|
isExporting: false,
|
||||||
|
canvasBackgroundColor: COLOR_PALETTE.white,
|
||||||
|
},
|
||||||
) as T["type"] extends keyof ElementShapes
|
) as T["type"] extends keyof ElementShapes
|
||||||
? ElementShapes[T["type"]]
|
? ElementShapes[T["type"]]
|
||||||
: Drawable | null;
|
: Drawable | null;
|
||||||
|
@ -262,6 +262,7 @@ export const exportToCanvas = async (
|
|||||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||||
},
|
},
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
imageCache,
|
imageCache,
|
||||||
renderGrid: false,
|
renderGrid: false,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
@ -429,9 +430,11 @@ export const exportToSvg = async (
|
|||||||
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
|
isExporting: true,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
renderEmbeddables: opts?.renderEmbeddables ?? false,
|
renderEmbeddables: opts?.renderEmbeddables ?? false,
|
||||||
frameRendering,
|
frameRendering,
|
||||||
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
tempScene.destroy();
|
tempScene.destroy();
|
||||||
|
@ -6,11 +6,13 @@ import {
|
|||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
StaticCanvasAppState,
|
StaticCanvasAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
export type StaticCanvasRenderConfig = {
|
export type StaticCanvasRenderConfig = {
|
||||||
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
imageCache: AppClassProperties["imageCache"];
|
imageCache: AppClassProperties["imageCache"];
|
||||||
@ -20,6 +22,16 @@ export type StaticCanvasRenderConfig = {
|
|||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SVGRenderConfig = {
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
isExporting: boolean;
|
||||||
|
exportWithDarkMode: boolean;
|
||||||
|
renderEmbeddables: boolean;
|
||||||
|
frameRendering: AppState["frameRendering"];
|
||||||
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
|
};
|
||||||
|
|
||||||
export type InteractiveCanvasRenderConfig = {
|
export type InteractiveCanvasRenderConfig = {
|
||||||
// collab-related state
|
// collab-related state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
Loading…
x
Reference in New Issue
Block a user