More Arrowheads: dot, bar (#2486)
Co-authored-by: Jed Fox <git@jedfox.com> Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
parent
7c7fb4903b
commit
c742225f43
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { getLanguage } from "../i18n";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
@ -16,7 +17,7 @@ import {
|
||||
} from "../scene";
|
||||
import { ButtonSelect } from "../components/ButtonSelect";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ButtonIconCycle } from "../components/ButtonIconCycle";
|
||||
import { IconPicker } from "../components/IconPicker";
|
||||
import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@ -43,7 +44,10 @@ import {
|
||||
SloppinessArchitectIcon,
|
||||
SloppinessArtistIcon,
|
||||
SloppinessCartoonistIcon,
|
||||
ArrowArrowheadIcon,
|
||||
ArrowheadArrowIcon,
|
||||
ArrowheadBarIcon,
|
||||
ArrowheadDotIcon,
|
||||
ArrowheadNoneIcon,
|
||||
} from "../components/icons";
|
||||
import { EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import colors from "../colors";
|
||||
@ -671,66 +675,124 @@ export const actionChangeArrowhead = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.arrowheads")}</legend>
|
||||
<div className="buttonList buttonListIcon">
|
||||
<ButtonIconCycle
|
||||
group="arrowhead_start"
|
||||
options={[
|
||||
{
|
||||
value: null,
|
||||
text: t("labels.arrowhead_none"),
|
||||
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "arrow",
|
||||
text: t("labels.arrowhead_arrow"),
|
||||
icon: (
|
||||
<ArrowArrowheadIcon
|
||||
appearance={appState.appearance}
|
||||
flip={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={getFormValue<Arrowhead | null>(
|
||||
elements,
|
||||
appState,
|
||||
(element) =>
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.startArrowhead
|
||||
: appState.currentItemArrowheads.start,
|
||||
appState.currentItemArrowheads.start,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "start", type: value })}
|
||||
/>
|
||||
<ButtonIconCycle
|
||||
group="arrowhead_end"
|
||||
options={[
|
||||
{
|
||||
value: null,
|
||||
text: t("labels.arrowhead_none"),
|
||||
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "arrow",
|
||||
text: t("labels.arrowhead_arrow"),
|
||||
icon: <ArrowArrowheadIcon appearance={appState.appearance} />,
|
||||
},
|
||||
]}
|
||||
value={getFormValue<Arrowhead | null>(
|
||||
elements,
|
||||
appState,
|
||||
(element) =>
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.endArrowhead
|
||||
: appState.currentItemArrowheads.end,
|
||||
appState.currentItemArrowheads.end,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "end", type: value })}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
),
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
const isRTL = getLanguage().rtl;
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.arrowheads")}</legend>
|
||||
<div className="iconSelectList">
|
||||
<IconPicker
|
||||
label="arrowhead_start"
|
||||
options={[
|
||||
{
|
||||
value: null,
|
||||
text: t("labels.arrowhead_none"),
|
||||
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
|
||||
keyBinding: "q",
|
||||
},
|
||||
{
|
||||
value: "arrow",
|
||||
text: t("labels.arrowhead_arrow"),
|
||||
icon: (
|
||||
<ArrowheadArrowIcon
|
||||
appearance={appState.appearance}
|
||||
flip={!isRTL}
|
||||
/>
|
||||
),
|
||||
keyBinding: "w",
|
||||
},
|
||||
{
|
||||
value: "bar",
|
||||
text: t("labels.arrowhead_bar"),
|
||||
icon: (
|
||||
<ArrowheadBarIcon
|
||||
appearance={appState.appearance}
|
||||
flip={!isRTL}
|
||||
/>
|
||||
),
|
||||
keyBinding: "e",
|
||||
},
|
||||
{
|
||||
value: "dot",
|
||||
text: t("labels.arrowhead_dot"),
|
||||
icon: (
|
||||
<ArrowheadDotIcon
|
||||
appearance={appState.appearance}
|
||||
flip={!isRTL}
|
||||
/>
|
||||
),
|
||||
keyBinding: "r",
|
||||
},
|
||||
]}
|
||||
value={getFormValue<Arrowhead | null>(
|
||||
elements,
|
||||
appState,
|
||||
(element) =>
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.startArrowhead
|
||||
: appState.currentItemArrowheads.start,
|
||||
appState.currentItemArrowheads.start,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "start", type: value })}
|
||||
/>
|
||||
<IconPicker
|
||||
label="arrowhead_end"
|
||||
group="arrowheads"
|
||||
options={[
|
||||
{
|
||||
value: null,
|
||||
text: t("labels.arrowhead_none"),
|
||||
keyBinding: "q",
|
||||
icon: <ArrowheadNoneIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "arrow",
|
||||
text: t("labels.arrowhead_arrow"),
|
||||
keyBinding: "w",
|
||||
icon: (
|
||||
<ArrowheadArrowIcon
|
||||
appearance={appState.appearance}
|
||||
flip={isRTL}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "bar",
|
||||
text: t("labels.arrowhead_bar"),
|
||||
keyBinding: "e",
|
||||
icon: (
|
||||
<ArrowheadBarIcon
|
||||
appearance={appState.appearance}
|
||||
flip={isRTL}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "dot",
|
||||
text: t("labels.arrowhead_dot"),
|
||||
keyBinding: "r",
|
||||
icon: (
|
||||
<ArrowheadDotIcon
|
||||
appearance={appState.appearance}
|
||||
flip={isRTL}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={getFormValue<Arrowhead | null>(
|
||||
elements,
|
||||
appState,
|
||||
(element) =>
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.endArrowhead
|
||||
: appState.currentItemArrowheads.end,
|
||||
appState.currentItemArrowheads.end,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "end", type: value })}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
137
src/components/IconPicker.scss
Normal file
137
src/components/IconPicker.scss
Normal file
@ -0,0 +1,137 @@
|
||||
@import "open-color/open-color.scss";
|
||||
|
||||
.excalidraw {
|
||||
.picker-container {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.picker {
|
||||
background: var(--popup-background-color);
|
||||
border: 0px solid transparentize($oc-white, 0.75);
|
||||
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.picker-container button,
|
||||
.picker button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
background-color: var(--button-gray-2);
|
||||
& svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin: 0;
|
||||
width: 36px;
|
||||
height: 18px;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.picker button {
|
||||
padding: 0.25rem 0.28rem 0.35rem 0.25rem;
|
||||
}
|
||||
|
||||
.picker-triangle {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
:root[dir="ltr"] & {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
right: 12px;
|
||||
}
|
||||
z-index: 10;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-style: solid;
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent transparentize($oc-black, 0.9);
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-style: solid;
|
||||
border-width: 0px 9px 10px;
|
||||
border-color: transparent transparent var(--popup-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-content {
|
||||
padding: 0.5rem;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.picker-keybinding {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.picker-type-canvasBackground .picker-keybinding {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.picker-type-elementBackground .picker-keybinding {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.picker-type-elementStroke .picker-keybinding {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
&.Appearance_dark {
|
||||
.picker-type-elementBackground .picker-keybinding {
|
||||
color: #000;
|
||||
}
|
||||
.picker-swatch[aria-label="transparent"] .picker-keybinding {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
188
src/components/IconPicker.tsx
Normal file
188
src/components/IconPicker.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
import React from "react";
|
||||
import { Popover } from "./Popover";
|
||||
|
||||
import "./IconPicker.scss";
|
||||
import { isArrowKey, KEYS } from "../keys";
|
||||
import { getLanguage } from "../i18n";
|
||||
|
||||
function Picker<T>({
|
||||
options,
|
||||
value,
|
||||
label,
|
||||
onChange,
|
||||
onClose,
|
||||
}: {
|
||||
label: string;
|
||||
value: T;
|
||||
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
||||
onChange: (value: T) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const rFirstItem = React.useRef<HTMLButtonElement>();
|
||||
const rActiveItem = React.useRef<HTMLButtonElement>();
|
||||
const rGallery = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// After the component is first mounted focus on first input
|
||||
if (rActiveItem.current) {
|
||||
rActiveItem.current.focus();
|
||||
} else if (rGallery.current) {
|
||||
rGallery.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
const pressedOption = options.find(
|
||||
(option) => option.keyBinding === event.key.toLowerCase(),
|
||||
)!;
|
||||
|
||||
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
|
||||
// Keybinding navigation
|
||||
const index = options.indexOf(pressedOption);
|
||||
(rGallery!.current!.children![index] as any).focus();
|
||||
event.preventDefault();
|
||||
} else if (event.key === KEYS.TAB) {
|
||||
// Tab navigation cycle through options. If the user tabs
|
||||
// away from the picker, close the picker. We need to use
|
||||
// a timeout here to let the stack clear before checking.
|
||||
setTimeout(() => {
|
||||
const active = rActiveItem.current;
|
||||
const docActive = document.activeElement;
|
||||
if (active !== docActive) {
|
||||
onClose();
|
||||
}
|
||||
}, 0);
|
||||
} else if (isArrowKey(event.key)) {
|
||||
// Arrow navigation
|
||||
const { activeElement } = document;
|
||||
const isRTL = getLanguage().rtl;
|
||||
const index = Array.prototype.indexOf.call(
|
||||
rGallery!.current!.children,
|
||||
activeElement,
|
||||
);
|
||||
if (index !== -1) {
|
||||
const length = options.length;
|
||||
let nextIndex = index;
|
||||
|
||||
switch (event.key) {
|
||||
// Select the next option
|
||||
case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
|
||||
case KEYS.ARROW_DOWN: {
|
||||
nextIndex = (index + 1) % length;
|
||||
break;
|
||||
}
|
||||
// Select the previous option
|
||||
case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
|
||||
case KEYS.ARROW_UP: {
|
||||
nextIndex = (length + index - 1) % length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(rGallery.current!.children![nextIndex] as any).focus();
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||
// Close on escape or enter
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`picker`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={label}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className="picker-content" ref={rGallery}>
|
||||
{options.map((option, i) => (
|
||||
<button
|
||||
className="picker-option"
|
||||
onClick={(event) => {
|
||||
(event.currentTarget as HTMLButtonElement).focus();
|
||||
onChange(option.value);
|
||||
}}
|
||||
title={`${option.text} — ${option.keyBinding.toUpperCase()}`}
|
||||
aria-label={option.text || "none"}
|
||||
aria-keyshortcuts={option.keyBinding}
|
||||
key={option.text}
|
||||
ref={(el) => {
|
||||
if (el && i === 0) {
|
||||
rFirstItem.current = el;
|
||||
}
|
||||
if (el && option.value === value) {
|
||||
rActiveItem.current = el;
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
onChange(option.value);
|
||||
}}
|
||||
>
|
||||
{option.icon}
|
||||
<span className="picker-keybinding">{option.keyBinding}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconPicker<T>({
|
||||
value,
|
||||
label,
|
||||
options,
|
||||
onChange,
|
||||
group = "",
|
||||
}: {
|
||||
label: string;
|
||||
value: T;
|
||||
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
|
||||
onChange: (value: T) => void;
|
||||
group?: string;
|
||||
}) {
|
||||
const [isActive, setActive] = React.useState(false);
|
||||
const rPickerButton = React.useRef<any>(null);
|
||||
const isRTL = getLanguage().rtl;
|
||||
|
||||
return (
|
||||
<label className={"picker-container"}>
|
||||
<button
|
||||
name={group}
|
||||
className={isActive ? "active" : ""}
|
||||
aria-label={label}
|
||||
onClick={() => setActive(!isActive)}
|
||||
ref={rPickerButton}
|
||||
>
|
||||
{options.find((option) => option.value === value)?.icon}
|
||||
</button>
|
||||
<React.Suspense fallback="">
|
||||
{isActive ? (
|
||||
<>
|
||||
<Popover
|
||||
onCloseRequest={(event) =>
|
||||
event.target !== rPickerButton.current && setActive(false)
|
||||
}
|
||||
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
|
||||
>
|
||||
<Picker
|
||||
options={options}
|
||||
value={value}
|
||||
label={label}
|
||||
onChange={onChange}
|
||||
onClose={() => {
|
||||
setActive(false);
|
||||
rPickerButton.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
<div className="picker-triangle" />
|
||||
</>
|
||||
) : null}
|
||||
</React.Suspense>
|
||||
</label>
|
||||
);
|
||||
}
|
@ -82,7 +82,7 @@ export const MobileMenu = ({
|
||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||
}}
|
||||
>
|
||||
<Island padding={3}>
|
||||
<Island padding={0}>
|
||||
{appState.openMenu === "canvas" ? (
|
||||
<Section className="App-mobile-menu" heading="canvasActions">
|
||||
<div className="panelColumn">
|
||||
|
@ -725,7 +725,23 @@ export const EdgeRoundIcon = React.memo(
|
||||
),
|
||||
);
|
||||
|
||||
export const ArrowArrowheadIcon = React.memo(
|
||||
export const ArrowheadNoneIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M6 10H34"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{
|
||||
width: 40,
|
||||
height: 20,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const ArrowheadArrowIcon = React.memo(
|
||||
({
|
||||
appearance,
|
||||
flip = false,
|
||||
@ -743,6 +759,48 @@ export const ArrowArrowheadIcon = React.memo(
|
||||
<path d="M34 10H6M34 10L27 5M34 10L27 15" />
|
||||
<path d="M27.5 5L34.5 10L27.5 15" />
|
||||
</g>,
|
||||
{ width: 40, height: 20, mirror: true },
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const ArrowheadDotIcon = React.memo(
|
||||
({
|
||||
appearance,
|
||||
flip = false,
|
||||
}: {
|
||||
appearance: "light" | "dark";
|
||||
flip?: boolean;
|
||||
}) =>
|
||||
createIcon(
|
||||
<g
|
||||
stroke={iconFillColor(appearance)}
|
||||
fill={iconFillColor(appearance)}
|
||||
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||
>
|
||||
<path d="M32 10L6 10" strokeWidth={2} />
|
||||
<circle r="4" transform="matrix(-1 0 0 1 30 10)" />
|
||||
</g>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const ArrowheadBarIcon = React.memo(
|
||||
({
|
||||
appearance,
|
||||
flip = false,
|
||||
}: {
|
||||
appearance: "light" | "dark";
|
||||
flip?: boolean;
|
||||
}) =>
|
||||
createIcon(
|
||||
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
||||
<path
|
||||
d="M34 10H5.99996M34 10L34 5M34 10L34 15"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</g>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
@ -84,6 +84,11 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.iconSelectList {
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.buttonList {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@ -236,6 +241,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: initial;
|
||||
|
||||
.panelColumn {
|
||||
padding: 8px 8px 0px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +258,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.App-mobile-menu {
|
||||
|
@ -212,7 +212,11 @@ export const getArrowheadPoints = (
|
||||
const nx = (x2 - x1) / distance;
|
||||
const ny = (y2 - y1) / distance;
|
||||
|
||||
const size = 30; // pixels (will differ for each arrowhead)
|
||||
const size = {
|
||||
arrow: 30,
|
||||
bar: 15,
|
||||
dot: 15,
|
||||
}[arrowhead]; // pixels (will differ for each arrowhead)
|
||||
|
||||
const length = element.points.reduce((total, [cx, cy], idx, points) => {
|
||||
const [px, py] = idx > 0 ? points[idx - 1] : [0, 0];
|
||||
@ -226,7 +230,15 @@ export const getArrowheadPoints = (
|
||||
const xs = x2 - nx * minSize;
|
||||
const ys = y2 - ny * minSize;
|
||||
|
||||
const angle = 20; // degrees
|
||||
if (arrowhead === "dot") {
|
||||
const r = Math.hypot(ys - y2, xs - x2);
|
||||
return [x2, y2, r];
|
||||
}
|
||||
|
||||
const angle = {
|
||||
arrow: 20,
|
||||
bar: 90,
|
||||
}[arrowhead]; // degrees
|
||||
|
||||
// Return points
|
||||
const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180);
|
||||
|
@ -98,7 +98,7 @@ export type PointBinding = {
|
||||
gap: number;
|
||||
};
|
||||
|
||||
export type Arrowhead = "arrow";
|
||||
export type Arrowhead = "arrow" | "bar" | "dot";
|
||||
|
||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
|
@ -31,6 +31,8 @@
|
||||
"arrowheads": "Arrowheads",
|
||||
"arrowhead_none": "None",
|
||||
"arrowhead_arrow": "Arrow",
|
||||
"arrowhead_bar": "Bar",
|
||||
"arrowhead_dot": "Dot",
|
||||
"fontSize": "Font size",
|
||||
"fontFamily": "Font family",
|
||||
"onlySelected": "Only selected",
|
||||
|
@ -356,6 +356,17 @@ const generateElementShape = (
|
||||
}
|
||||
|
||||
// Other arrowheads here...
|
||||
if (arrowhead === "dot") {
|
||||
const [x, y, r] = arrowheadPoints;
|
||||
|
||||
return [
|
||||
generator.circle(x, y, r, {
|
||||
...options,
|
||||
fill: element.strokeColor,
|
||||
fillStyle: "solid",
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// Arrow arrowheads
|
||||
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||
|
Loading…
x
Reference in New Issue
Block a user