feat: sharpness (#1931)
* feat: sharpness * feat: fill sharp lines, et al. * fix: rotated positioning * chore: simplify path with Q * fix: hit test inside sharp elements * make sharp / round buttons work properly * fix tsc tests * update snapshots * update snapshots * fix: sharp arrow creation error * fix merge and test * avoid type assertion * remove duplicate helper Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
930813387b
commit
41cb1fbeba
@ -8,6 +8,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
|
getTargetElement,
|
||||||
|
canChangeSharpness,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { ButtonSelect } from "../components/ButtonSelect";
|
import { ButtonSelect } from "../components/ButtonSelect";
|
||||||
import {
|
import {
|
||||||
@ -15,6 +17,7 @@ import {
|
|||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
getNonDeletedElements,
|
getNonDeletedElements,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
|
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -450,3 +453,59 @@ export const actionChangeTextAlign = register({
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionChangeSharpness = register({
|
||||||
|
name: "changeSharpness",
|
||||||
|
perform: (elements, appState, value) => {
|
||||||
|
const targetElements = getTargetElement(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const shouldUpdateForNonLinearElements = targetElements.length
|
||||||
|
? targetElements.every((e) => !isLinearElement(e))
|
||||||
|
: !isLinearElementType(appState.elementType);
|
||||||
|
const shouldUpdateForLinearElements = targetElements.length
|
||||||
|
? targetElements.every(isLinearElement)
|
||||||
|
: isLinearElementType(appState.elementType);
|
||||||
|
return {
|
||||||
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
|
newElementWith(el, {
|
||||||
|
strokeSharpness: value,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
currentItemStrokeSharpness: shouldUpdateForNonLinearElements
|
||||||
|
? value
|
||||||
|
: appState.currentItemStrokeSharpness,
|
||||||
|
currentItemLinearStrokeSharpness: shouldUpdateForLinearElements
|
||||||
|
? value
|
||||||
|
: appState.currentItemLinearStrokeSharpness,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.edges")}</legend>
|
||||||
|
<ButtonSelect
|
||||||
|
group="edges"
|
||||||
|
options={[
|
||||||
|
{ value: "sharp", text: t("labels.sharp") },
|
||||||
|
{ value: "round", text: t("labels.round") },
|
||||||
|
]}
|
||||||
|
value={getFormValue(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) => element.strokeSharpness,
|
||||||
|
(canChangeSharpness(appState.elementType) &&
|
||||||
|
(isLinearElementType(appState.elementType)
|
||||||
|
? appState.currentItemLinearStrokeSharpness
|
||||||
|
: appState.currentItemStrokeSharpness)) ||
|
||||||
|
null,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
@ -63,7 +63,8 @@ export type ActionName =
|
|||||||
| "group"
|
| "group"
|
||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "goToCollaborator"
|
| "goToCollaborator"
|
||||||
| "addToLibrary";
|
| "addToLibrary"
|
||||||
|
| "changeSharpness";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
|
@ -36,6 +36,8 @@ export const getDefaultAppState = (): Omit<
|
|||||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
|
currentItemStrokeSharpness: "sharp",
|
||||||
|
currentItemLinearStrokeSharpness: "round",
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
scrollX: 0 as FlooredNumber,
|
scrollX: 0 as FlooredNumber,
|
||||||
scrollY: 0 as FlooredNumber,
|
scrollY: 0 as FlooredNumber,
|
||||||
@ -96,6 +98,8 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
currentItemStrokeStyle: { browser: true, export: false },
|
currentItemStrokeStyle: { browser: true, export: false },
|
||||||
currentItemStrokeWidth: { browser: true, export: false },
|
currentItemStrokeWidth: { browser: true, export: false },
|
||||||
currentItemTextAlign: { browser: true, export: false },
|
currentItemTextAlign: { browser: true, export: false },
|
||||||
|
currentItemStrokeSharpness: { browser: true, export: false },
|
||||||
|
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||||
cursorButton: { browser: true, export: false },
|
cursorButton: { browser: true, export: false },
|
||||||
cursorX: { browser: true, export: false },
|
cursorX: { browser: true, export: false },
|
||||||
cursorY: { browser: true, export: false },
|
cursorY: { browser: true, export: false },
|
||||||
|
@ -164,6 +164,7 @@ export function renderSpreadsheet(
|
|||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
roughness: appState.currentItemRoughness,
|
roughness: appState.currentItemRoughness,
|
||||||
opacity: appState.currentItemOpacity,
|
opacity: appState.currentItemOpacity,
|
||||||
|
strokeSharpness: appState.currentItemStrokeSharpness,
|
||||||
text: min.toLocaleString(),
|
text: min.toLocaleString(),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: appState.currentItemFontFamily,
|
fontFamily: appState.currentItemFontFamily,
|
||||||
@ -181,6 +182,7 @@ export function renderSpreadsheet(
|
|||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
roughness: appState.currentItemRoughness,
|
roughness: appState.currentItemRoughness,
|
||||||
opacity: appState.currentItemOpacity,
|
opacity: appState.currentItemOpacity,
|
||||||
|
strokeSharpness: appState.currentItemStrokeSharpness,
|
||||||
text: max.toLocaleString(),
|
text: max.toLocaleString(),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: appState.currentItemFontFamily,
|
fontFamily: appState.currentItemFontFamily,
|
||||||
@ -207,6 +209,7 @@ export function renderSpreadsheet(
|
|||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
roughness: appState.currentItemRoughness,
|
roughness: appState.currentItemRoughness,
|
||||||
opacity: appState.currentItemOpacity,
|
opacity: appState.currentItemOpacity,
|
||||||
|
strokeSharpness: appState.currentItemStrokeSharpness,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,6 +229,7 @@ export function renderSpreadsheet(
|
|||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
roughness: appState.currentItemRoughness,
|
roughness: appState.currentItemRoughness,
|
||||||
opacity: appState.currentItemOpacity,
|
opacity: appState.currentItemOpacity,
|
||||||
|
strokeSharpness: appState.currentItemStrokeSharpness,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: appState.currentItemFontFamily,
|
fontFamily: appState.currentItemFontFamily,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@ -247,6 +251,7 @@ export function renderSpreadsheet(
|
|||||||
strokeStyle: appState.currentItemStrokeStyle,
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
roughness: appState.currentItemRoughness,
|
roughness: appState.currentItemRoughness,
|
||||||
opacity: appState.currentItemOpacity,
|
opacity: appState.currentItemOpacity,
|
||||||
|
strokeSharpness: appState.currentItemStrokeSharpness,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontFamily: appState.currentItemFontFamily,
|
fontFamily: appState.currentItemFontFamily,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
@ -2,7 +2,13 @@ import React from "react";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { hasBackground, hasStroke, hasText, getTargetElement } from "../scene";
|
import {
|
||||||
|
hasBackground,
|
||||||
|
hasStroke,
|
||||||
|
canChangeSharpness,
|
||||||
|
hasText,
|
||||||
|
getTargetElement,
|
||||||
|
} from "../scene";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
@ -50,6 +56,11 @@ export const SelectedShapeActions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(canChangeSharpness(elementType) ||
|
||||||
|
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||||
|
<>{renderAction("changeSharpness")}</>
|
||||||
|
)}
|
||||||
|
|
||||||
{(hasText(elementType) ||
|
{(hasText(elementType) ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some((element) => hasText(element.type))) && (
|
||||||
<>
|
<>
|
||||||
|
@ -1057,6 +1057,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
strokeStyle: this.state.currentItemStrokeStyle,
|
strokeStyle: this.state.currentItemStrokeStyle,
|
||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
||||||
text: text,
|
text: text,
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
@ -1771,6 +1772,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
strokeStyle: this.state.currentItemStrokeStyle,
|
strokeStyle: this.state.currentItemStrokeStyle,
|
||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
||||||
text: "",
|
text: "",
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
@ -2672,6 +2674,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
strokeStyle: this.state.currentItemStrokeStyle,
|
strokeStyle: this.state.currentItemStrokeStyle,
|
||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
|
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||||
});
|
});
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
@ -2719,6 +2722,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
strokeStyle: this.state.currentItemStrokeStyle,
|
strokeStyle: this.state.currentItemStrokeStyle,
|
||||||
roughness: this.state.currentItemRoughness,
|
roughness: this.state.currentItemRoughness,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (element.type === "selection") {
|
if (element.type === "selection") {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DataState } from "./types";
|
import { DataState } from "./types";
|
||||||
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
||||||
|
import { isLinearElementType } from "../element/typeChecks";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import {
|
import {
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
@ -49,6 +50,9 @@ function migrateElementWithProperties<T extends ExcalidrawElement>(
|
|||||||
height: element.height || 0,
|
height: element.height || 0,
|
||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
groupIds: element.groupIds ?? [],
|
groupIds: element.groupIds ?? [],
|
||||||
|
strokeSharpness:
|
||||||
|
element.strokeSharpness ??
|
||||||
|
(isLinearElementType(element.type) ? "round" : "sharp"),
|
||||||
boundElementIds: element.boundElementIds ?? [],
|
boundElementIds: element.boundElementIds ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,6 +165,9 @@ export const getArrowPoints = (
|
|||||||
shape: Drawable[],
|
shape: Drawable[],
|
||||||
) => {
|
) => {
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
if (ops.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const data = ops[ops.length - 1].data;
|
const data = ops[ops.length - 1].data;
|
||||||
const p3 = [data[4], data[5]] as Point;
|
const p3 = [data[4], data[5]] as Point;
|
||||||
@ -339,10 +342,13 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
const curve = gen.curve(
|
const curve =
|
||||||
|
element.strokeSharpness === "sharp"
|
||||||
|
? gen.linearPath(
|
||||||
points as [number, number][],
|
points as [number, number][],
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
)
|
||||||
|
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
||||||
const ops = getCurvePathOps(curve);
|
const ops = getCurvePathOps(curve);
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
return [
|
return [
|
||||||
@ -356,13 +362,17 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
export const getElementPointsCoords = (
|
export const getElementPointsCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
points: readonly (readonly [number, number])[],
|
points: readonly (readonly [number, number])[],
|
||||||
|
sharpness: ExcalidrawElement["strokeSharpness"],
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
// This might be computationally heavey
|
// This might be computationally heavey
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
const curve = gen.curve(
|
const curve =
|
||||||
|
sharpness === "sharp"
|
||||||
|
? gen.linearPath(
|
||||||
points as [number, number][],
|
points as [number, number][],
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
)
|
||||||
|
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
||||||
const ops = getCurvePathOps(curve);
|
const ops = getCurvePathOps(curve);
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
return [
|
return [
|
||||||
|
@ -267,7 +267,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
|
|||||||
|
|
||||||
if (args.check === isInsideCheck) {
|
if (args.check === isInsideCheck) {
|
||||||
const hit = shape.some((subshape) =>
|
const hit = shape.some((subshape) =>
|
||||||
hitTestCurveInside(subshape, relX, relY),
|
hitTestCurveInside(subshape, relX, relY, element.strokeSharpness),
|
||||||
);
|
);
|
||||||
if (hit) {
|
if (hit) {
|
||||||
return true;
|
return true;
|
||||||
@ -688,22 +688,33 @@ const pointInBezierEquation = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hitTestCurveInside = (drawable: Drawable, x: number, y: number) => {
|
const hitTestCurveInside = (
|
||||||
|
drawable: Drawable,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
sharpness: ExcalidrawElement["strokeSharpness"],
|
||||||
|
) => {
|
||||||
const ops = getCurvePathOps(drawable);
|
const ops = getCurvePathOps(drawable);
|
||||||
const points: Point[] = [];
|
const points: Point[] = [];
|
||||||
|
let odd = false; // select one line out of double lines
|
||||||
for (const operation of ops) {
|
for (const operation of ops) {
|
||||||
if (operation.op === "move") {
|
if (operation.op === "move") {
|
||||||
if (points.length) {
|
odd = !odd;
|
||||||
break;
|
if (odd) {
|
||||||
}
|
|
||||||
points.push([operation.data[0], operation.data[1]]);
|
points.push([operation.data[0], operation.data[1]]);
|
||||||
|
}
|
||||||
} else if (operation.op === "bcurveTo") {
|
} else if (operation.op === "bcurveTo") {
|
||||||
|
if (odd) {
|
||||||
points.push([operation.data[0], operation.data[1]]);
|
points.push([operation.data[0], operation.data[1]]);
|
||||||
points.push([operation.data[2], operation.data[3]]);
|
points.push([operation.data[2], operation.data[3]]);
|
||||||
points.push([operation.data[4], operation.data[5]]);
|
points.push([operation.data[4], operation.data[5]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (points.length >= 4) {
|
if (points.length >= 4) {
|
||||||
|
if (sharpness === "sharp") {
|
||||||
|
return isPointInPolygon(points, x, y);
|
||||||
|
}
|
||||||
const polygonPoints = pointsOnBezierCurves(points as any, 10, 5);
|
const polygonPoints = pointsOnBezierCurves(points as any, 10, 5);
|
||||||
return isPointInPolygon(polygonPoints, x, y);
|
return isPointInPolygon(polygonPoints, x, y);
|
||||||
}
|
}
|
||||||
|
@ -508,8 +508,16 @@ export class LinearElementEditor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(
|
||||||
const prevCoords = getElementPointsCoords(element, points);
|
element,
|
||||||
|
nextPoints,
|
||||||
|
element.strokeSharpness || "round",
|
||||||
|
);
|
||||||
|
const prevCoords = getElementPointsCoords(
|
||||||
|
element,
|
||||||
|
points,
|
||||||
|
element.strokeSharpness || "round",
|
||||||
|
);
|
||||||
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
||||||
const nextCenterY = (nextCoords[1] + nextCoords[3]) / 2;
|
const nextCenterY = (nextCoords[1] + nextCoords[3]) / 2;
|
||||||
const prevCenterX = (prevCoords[0] + prevCoords[2]) / 2;
|
const prevCenterX = (prevCoords[0] + prevCoords[2]) / 2;
|
||||||
|
@ -31,6 +31,7 @@ it("clones arrow element", () => {
|
|||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
|
strokeSharpness: "round",
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
});
|
});
|
||||||
@ -75,6 +76,7 @@ it("clones text element", () => {
|
|||||||
fillStyle: "hachure",
|
fillStyle: "hachure",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
|
strokeSharpness: "round",
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
text: "hello",
|
text: "hello",
|
||||||
|
@ -46,6 +46,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
height = 0,
|
height = 0,
|
||||||
angle = 0,
|
angle = 0,
|
||||||
groupIds = [],
|
groupIds = [],
|
||||||
|
strokeSharpness,
|
||||||
boundElementIds = null,
|
boundElementIds = null,
|
||||||
...rest
|
...rest
|
||||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||||
@ -65,6 +66,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
roughness,
|
roughness,
|
||||||
opacity,
|
opacity,
|
||||||
groupIds,
|
groupIds,
|
||||||
|
strokeSharpness,
|
||||||
seed: rest.seed ?? randomInteger(),
|
seed: rest.seed ?? randomInteger(),
|
||||||
version: rest.version || 1,
|
version: rest.version || 1,
|
||||||
versionNonce: rest.versionNonce ?? 0,
|
versionNonce: rest.versionNonce ?? 0,
|
||||||
|
@ -12,6 +12,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
fillStyle: string;
|
fillStyle: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
strokeStyle: "solid" | "dashed" | "dotted";
|
strokeStyle: "solid" | "dashed" | "dotted";
|
||||||
|
strokeSharpness: "round" | "sharp";
|
||||||
roughness: number;
|
roughness: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -25,6 +25,9 @@
|
|||||||
"sloppiness": "Sloppiness",
|
"sloppiness": "Sloppiness",
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
"textAlign": "Text align",
|
"textAlign": "Text align",
|
||||||
|
"edges": "Edges",
|
||||||
|
"sharp": "Sharp",
|
||||||
|
"round": "Round",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"onlySelected": "Only selected",
|
"onlySelected": "Only selected",
|
||||||
|
@ -240,6 +240,19 @@ const generateElementShape = (
|
|||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
if (element.strokeSharpness === "round") {
|
||||||
|
const w = element.width;
|
||||||
|
const h = element.height;
|
||||||
|
const r = Math.min(w, h) * 0.25;
|
||||||
|
shape = generator.path(
|
||||||
|
`M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${
|
||||||
|
h - r
|
||||||
|
} Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
|
||||||
|
h - r
|
||||||
|
} L 0 ${r} Q 0 0, ${r} 0`,
|
||||||
|
generateRoughOptions(element),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
shape = generator.rectangle(
|
shape = generator.rectangle(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@ -247,7 +260,7 @@ const generateElementShape = (
|
|||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "diamond": {
|
case "diamond": {
|
||||||
const [
|
const [
|
||||||
@ -291,11 +304,23 @@ const generateElementShape = (
|
|||||||
|
|
||||||
// curve is always the first element
|
// curve is always the first element
|
||||||
// this simplifies finding the curve for an element
|
// this simplifies finding the curve for an element
|
||||||
|
if (element.strokeSharpness === "sharp") {
|
||||||
|
if (options.fill) {
|
||||||
|
shape = [generator.polygon(points as [number, number][], options)];
|
||||||
|
} else {
|
||||||
|
shape = [
|
||||||
|
generator.linearPath(points as [number, number][], options),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
shape = [generator.curve(points as [number, number][], options)];
|
shape = [generator.curve(points as [number, number][], options)];
|
||||||
|
}
|
||||||
|
|
||||||
// add lines only in arrow
|
// add lines only in arrow
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element, shape);
|
const arrowPoints = getArrowPoints(element, shape);
|
||||||
|
if (arrowPoints) {
|
||||||
|
const [x2, y2, x3, y3, x4, y4] = arrowPoints;
|
||||||
// for dotted arrows caps, reduce gap to make it more legible
|
// for dotted arrows caps, reduce gap to make it more legible
|
||||||
if (element.strokeStyle === "dotted") {
|
if (element.strokeStyle === "dotted") {
|
||||||
options.strokeLineDash = [3, 4];
|
options.strokeLineDash = [3, 4];
|
||||||
@ -310,6 +335,7 @@ const generateElementShape = (
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
|
@ -20,6 +20,12 @@ export const hasStroke = (type: string) =>
|
|||||||
type === "draw" ||
|
type === "draw" ||
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
|
export const canChangeSharpness = (type: string) =>
|
||||||
|
type === "rectangle" ||
|
||||||
|
type === "arrow" ||
|
||||||
|
type === "draw" ||
|
||||||
|
type === "line";
|
||||||
|
|
||||||
export const hasText = (type: string) => type === "text";
|
export const hasText = (type: string) => type === "text";
|
||||||
|
|
||||||
export const getElementAtPosition = (
|
export const getElementAtPosition = (
|
||||||
|
@ -165,5 +165,6 @@ const getWatermarkElement = (maxX: number, maxY: number) => {
|
|||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
|
strokeSharpness: "sharp",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ export { normalizeScroll, calculateScrollCenter } from "./scroll";
|
|||||||
export {
|
export {
|
||||||
hasBackground,
|
hasBackground,
|
||||||
hasStroke,
|
hasStroke,
|
||||||
|
canChangeSharpness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
getElementContainingPosition,
|
getElementContainingPosition,
|
||||||
hasText,
|
hasText,
|
||||||
|
@ -29,6 +29,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
@ -56,6 +57,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
@ -83,6 +85,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
@ -121,6 +124,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
@ -148,6 +152,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
@ -14,6 +14,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 401146281,
|
"seed": 401146281,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
@ -39,6 +40,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
@ -64,6 +66,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
@ -34,6 +34,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
@ -79,6 +80,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
@ -39,6 +40,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
@ -27,6 +27,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
@ -65,6 +66,7 @@ Object {
|
|||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
@ -90,6 +92,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
@ -115,6 +118,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
@ -140,6 +144,7 @@ Object {
|
|||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
|
"strokeSharpness": "sharp",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
@ -37,6 +37,7 @@ const populateElements = (
|
|||||||
fillStyle: h.state.currentItemFillStyle,
|
fillStyle: h.state.currentItemFillStyle,
|
||||||
strokeWidth: h.state.currentItemStrokeWidth,
|
strokeWidth: h.state.currentItemStrokeWidth,
|
||||||
strokeStyle: h.state.currentItemStrokeStyle,
|
strokeStyle: h.state.currentItemStrokeStyle,
|
||||||
|
strokeSharpness: h.state.currentItemStrokeSharpness,
|
||||||
roughness: h.state.currentItemRoughness,
|
roughness: h.state.currentItemRoughness,
|
||||||
opacity: h.state.currentItemOpacity,
|
opacity: h.state.currentItemOpacity,
|
||||||
});
|
});
|
||||||
|
@ -56,6 +56,8 @@ export type AppState = {
|
|||||||
currentItemFontFamily: FontFamily;
|
currentItemFontFamily: FontFamily;
|
||||||
currentItemFontSize: number;
|
currentItemFontSize: number;
|
||||||
currentItemTextAlign: TextAlign;
|
currentItemTextAlign: TextAlign;
|
||||||
|
currentItemStrokeSharpness: ExcalidrawElement["strokeSharpness"];
|
||||||
|
currentItemLinearStrokeSharpness: ExcalidrawElement["strokeSharpness"];
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
scrollX: FlooredNumber;
|
scrollX: FlooredNumber;
|
||||||
scrollY: FlooredNumber;
|
scrollY: FlooredNumber;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user