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:
Daishi Kato 2020-08-15 00:59:43 +09:00 committed by GitHub
parent 930813387b
commit 41cb1fbeba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 841 additions and 42 deletions

View file

@ -165,6 +165,9 @@ export const getArrowPoints = (
shape: Drawable[],
) => {
const ops = getCurvePathOps(shape[0]);
if (ops.length < 1) {
return null;
}
const data = ops[ops.length - 1].data;
const p3 = [data[4], data[5]] as Point;
@ -339,10 +342,13 @@ export const getResizedElementAbsoluteCoords = (
);
const gen = rough.generator();
const curve = gen.curve(
points as [number, number][],
generateRoughOptions(element),
);
const curve =
element.strokeSharpness === "sharp"
? gen.linearPath(
points as [number, number][],
generateRoughOptions(element),
)
: gen.curve(points as [number, number][], generateRoughOptions(element));
const ops = getCurvePathOps(curve);
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
return [
@ -356,13 +362,17 @@ export const getResizedElementAbsoluteCoords = (
export const getElementPointsCoords = (
element: ExcalidrawLinearElement,
points: readonly (readonly [number, number])[],
sharpness: ExcalidrawElement["strokeSharpness"],
): [number, number, number, number] => {
// This might be computationally heavey
const gen = rough.generator();
const curve = gen.curve(
points as [number, number][],
generateRoughOptions(element),
);
const curve =
sharpness === "sharp"
? gen.linearPath(
points as [number, number][],
generateRoughOptions(element),
)
: gen.curve(points as [number, number][], generateRoughOptions(element));
const ops = getCurvePathOps(curve);
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
return [

View file

@ -267,7 +267,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
if (args.check === isInsideCheck) {
const hit = shape.some((subshape) =>
hitTestCurveInside(subshape, relX, relY),
hitTestCurveInside(subshape, relX, relY, element.strokeSharpness),
);
if (hit) {
return true;
@ -688,22 +688,33 @@ const pointInBezierEquation = (
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 points: Point[] = [];
let odd = false; // select one line out of double lines
for (const operation of ops) {
if (operation.op === "move") {
if (points.length) {
break;
odd = !odd;
if (odd) {
points.push([operation.data[0], operation.data[1]]);
}
points.push([operation.data[0], operation.data[1]]);
} else if (operation.op === "bcurveTo") {
points.push([operation.data[0], operation.data[1]]);
points.push([operation.data[2], operation.data[3]]);
points.push([operation.data[4], operation.data[5]]);
if (odd) {
points.push([operation.data[0], operation.data[1]]);
points.push([operation.data[2], operation.data[3]]);
points.push([operation.data[4], operation.data[5]]);
}
}
}
if (points.length >= 4) {
if (sharpness === "sharp") {
return isPointInPolygon(points, x, y);
}
const polygonPoints = pointsOnBezierCurves(points as any, 10, 5);
return isPointInPolygon(polygonPoints, x, y);
}

View file

@ -508,8 +508,16 @@ export class LinearElementEditor {
});
}
const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, points);
const nextCoords = getElementPointsCoords(
element,
nextPoints,
element.strokeSharpness || "round",
);
const prevCoords = getElementPointsCoords(
element,
points,
element.strokeSharpness || "round",
);
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
const nextCenterY = (nextCoords[1] + nextCoords[3]) / 2;
const prevCenterX = (prevCoords[0] + prevCoords[2]) / 2;

View file

@ -31,6 +31,7 @@ it("clones arrow element", () => {
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
strokeSharpness: "round",
roughness: 1,
opacity: 100,
});
@ -75,6 +76,7 @@ it("clones text element", () => {
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
strokeSharpness: "round",
roughness: 1,
opacity: 100,
text: "hello",

View file

@ -46,6 +46,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
height = 0,
angle = 0,
groupIds = [],
strokeSharpness,
boundElementIds = null,
...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
@ -65,6 +66,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
roughness,
opacity,
groupIds,
strokeSharpness,
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,
versionNonce: rest.versionNonce ?? 0,

View file

@ -12,6 +12,7 @@ type _ExcalidrawElementBase = Readonly<{
fillStyle: string;
strokeWidth: number;
strokeStyle: "solid" | "dashed" | "dotted";
strokeSharpness: "round" | "sharp";
roughness: number;
opacity: number;
width: number;