Kaynağa Gözat

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>
Daishi Kato 4 yıl önce
ebeveyn
işleme
41cb1fbeba

+ 59 - 0
src/actions/actionProperties.tsx

@@ -8,6 +8,8 @@ import {
 import {
   getCommonAttributeOfSelectedElements,
   isSomeElementSelected,
+  getTargetElement,
+  canChangeSharpness,
 } from "../scene";
 import { ButtonSelect } from "../components/ButtonSelect";
 import {
@@ -15,6 +17,7 @@ import {
   redrawTextBoundingBox,
   getNonDeletedElements,
 } from "../element";
+import { isLinearElement, isLinearElementType } from "../element/typeChecks";
 import { ColorPicker } from "../components/ColorPicker";
 import { AppState } from "../../src/types";
 import { t } from "../i18n";
@@ -450,3 +453,59 @@ export const actionChangeTextAlign = register({
     </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>
+  ),
+});

+ 2 - 1
src/actions/types.ts

@@ -63,7 +63,8 @@ export type ActionName =
   | "group"
   | "ungroup"
   | "goToCollaborator"
-  | "addToLibrary";
+  | "addToLibrary"
+  | "changeSharpness";
 
 export interface Action {
   name: ActionName;

+ 4 - 0
src/appState.ts

@@ -36,6 +36,8 @@ export const getDefaultAppState = (): Omit<
     currentItemFontSize: DEFAULT_FONT_SIZE,
     currentItemFontFamily: DEFAULT_FONT_FAMILY,
     currentItemTextAlign: DEFAULT_TEXT_ALIGN,
+    currentItemStrokeSharpness: "sharp",
+    currentItemLinearStrokeSharpness: "round",
     viewBackgroundColor: oc.white,
     scrollX: 0 as FlooredNumber,
     scrollY: 0 as FlooredNumber,
@@ -96,6 +98,8 @@ const APP_STATE_STORAGE_CONF = (<
   currentItemStrokeStyle: { browser: true, export: false },
   currentItemStrokeWidth: { browser: true, export: false },
   currentItemTextAlign: { browser: true, export: false },
+  currentItemStrokeSharpness: { browser: true, export: false },
+  currentItemLinearStrokeSharpness: { browser: true, export: false },
   cursorButton: { browser: true, export: false },
   cursorX: { browser: true, export: false },
   cursorY: { browser: true, export: false },

+ 5 - 0
src/charts.ts

@@ -164,6 +164,7 @@ export function renderSpreadsheet(
     strokeStyle: appState.currentItemStrokeStyle,
     roughness: appState.currentItemRoughness,
     opacity: appState.currentItemOpacity,
+    strokeSharpness: appState.currentItemStrokeSharpness,
     text: min.toLocaleString(),
     fontSize: 16,
     fontFamily: appState.currentItemFontFamily,
@@ -181,6 +182,7 @@ export function renderSpreadsheet(
     strokeStyle: appState.currentItemStrokeStyle,
     roughness: appState.currentItemRoughness,
     opacity: appState.currentItemOpacity,
+    strokeSharpness: appState.currentItemStrokeSharpness,
     text: max.toLocaleString(),
     fontSize: 16,
     fontFamily: appState.currentItemFontFamily,
@@ -207,6 +209,7 @@ export function renderSpreadsheet(
       strokeStyle: appState.currentItemStrokeStyle,
       roughness: appState.currentItemRoughness,
       opacity: appState.currentItemOpacity,
+      strokeSharpness: appState.currentItemStrokeSharpness,
     });
   });
 
@@ -226,6 +229,7 @@ export function renderSpreadsheet(
         strokeStyle: appState.currentItemStrokeStyle,
         roughness: appState.currentItemRoughness,
         opacity: appState.currentItemOpacity,
+        strokeSharpness: appState.currentItemStrokeSharpness,
         fontSize: 16,
         fontFamily: appState.currentItemFontFamily,
         textAlign: "center",
@@ -247,6 +251,7 @@ export function renderSpreadsheet(
         strokeStyle: appState.currentItemStrokeStyle,
         roughness: appState.currentItemRoughness,
         opacity: appState.currentItemOpacity,
+        strokeSharpness: appState.currentItemStrokeSharpness,
         fontSize: 20,
         fontFamily: appState.currentItemFontFamily,
         textAlign: "center",

+ 12 - 1
src/components/Actions.tsx

@@ -2,7 +2,13 @@ import React from "react";
 import { AppState } from "../types";
 import { ExcalidrawElement } from "../element/types";
 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 { SHAPES } from "../shapes";
 import { ToolButton } from "./ToolButton";
@@ -50,6 +56,11 @@ export const SelectedShapeActions = ({
         </>
       )}
 
+      {(canChangeSharpness(elementType) ||
+        targetElements.some((element) => canChangeSharpness(element.type))) && (
+        <>{renderAction("changeSharpness")}</>
+      )}
+
       {(hasText(elementType) ||
         targetElements.some((element) => hasText(element.type))) && (
         <>

+ 4 - 0
src/components/App.tsx

@@ -1057,6 +1057,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       strokeStyle: this.state.currentItemStrokeStyle,
       roughness: this.state.currentItemRoughness,
       opacity: this.state.currentItemOpacity,
+      strokeSharpness: this.state.currentItemStrokeSharpness,
       text: text,
       fontSize: this.state.currentItemFontSize,
       fontFamily: this.state.currentItemFontFamily,
@@ -1771,6 +1772,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           strokeStyle: this.state.currentItemStrokeStyle,
           roughness: this.state.currentItemRoughness,
           opacity: this.state.currentItemOpacity,
+          strokeSharpness: this.state.currentItemStrokeSharpness,
           text: "",
           fontSize: this.state.currentItemFontSize,
           fontFamily: this.state.currentItemFontFamily,
@@ -2672,6 +2674,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         strokeStyle: this.state.currentItemStrokeStyle,
         roughness: this.state.currentItemRoughness,
         opacity: this.state.currentItemOpacity,
+        strokeSharpness: this.state.currentItemLinearStrokeSharpness,
       });
       this.setState((prevState) => ({
         selectedElementIds: {
@@ -2719,6 +2722,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       strokeStyle: this.state.currentItemStrokeStyle,
       roughness: this.state.currentItemRoughness,
       opacity: this.state.currentItemOpacity,
+      strokeSharpness: this.state.currentItemStrokeSharpness,
     });
 
     if (element.type === "selection") {

+ 4 - 0
src/data/restore.ts

@@ -6,6 +6,7 @@ import {
 import { AppState } from "../types";
 import { DataState } from "./types";
 import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
+import { isLinearElementType } from "../element/typeChecks";
 import { randomId } from "../random";
 import {
   FONT_FAMILY,
@@ -49,6 +50,9 @@ function migrateElementWithProperties<T extends ExcalidrawElement>(
     height: element.height || 0,
     seed: element.seed ?? 1,
     groupIds: element.groupIds ?? [],
+    strokeSharpness:
+      element.strokeSharpness ??
+      (isLinearElementType(element.type) ? "round" : "sharp"),
     boundElementIds: element.boundElementIds ?? [],
   };
 

+ 18 - 8
src/element/bounds.ts

@@ -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 [

+ 19 - 8
src/element/collision.ts

@@ -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);
   }

+ 10 - 2
src/element/linearElementEditor.ts

@@ -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;

+ 2 - 0
src/element/newElement.test.ts

@@ -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",

+ 2 - 0
src/element/newElement.ts

@@ -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,

+ 1 - 0
src/element/types.ts

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

+ 3 - 0
src/locales/en.json

@@ -25,6 +25,9 @@
     "sloppiness": "Sloppiness",
     "opacity": "Opacity",
     "textAlign": "Text align",
+    "edges": "Edges",
+    "sharp": "Sharp",
+    "round": "Round",
     "fontSize": "Font size",
     "fontFamily": "Font family",
     "onlySelected": "Only selected",

+ 48 - 22
src/renderer/renderElement.ts

@@ -240,14 +240,27 @@ const generateElementShape = (
 
     switch (element.type) {
       case "rectangle":
-        shape = generator.rectangle(
-          0,
-          0,
-          element.width,
-          element.height,
-          generateRoughOptions(element),
-        );
-
+        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(
+            0,
+            0,
+            element.width,
+            element.height,
+            generateRoughOptions(element),
+          );
+        }
         break;
       case "diamond": {
         const [
@@ -291,24 +304,37 @@ const generateElementShape = (
 
         // curve is always the first element
         // this simplifies finding the curve for an element
-        shape = [generator.curve(points as [number, number][], options)];
+        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)];
+        }
 
         // add lines only in arrow
         if (element.type === "arrow") {
-          const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element, shape);
-          // for dotted arrows caps, reduce gap to make it more legible
-          if (element.strokeStyle === "dotted") {
-            options.strokeLineDash = [3, 4];
-            // for solid/dashed, keep solid arrow cap
-          } else {
-            delete options.strokeLineDash;
+          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
+            if (element.strokeStyle === "dotted") {
+              options.strokeLineDash = [3, 4];
+              // for solid/dashed, keep solid arrow cap
+            } else {
+              delete options.strokeLineDash;
+            }
+            shape.push(
+              ...[
+                generator.line(x3, y3, x2, y2, options),
+                generator.line(x4, y4, x2, y2, options),
+              ],
+            );
           }
-          shape.push(
-            ...[
-              generator.line(x3, y3, x2, y2, options),
-              generator.line(x4, y4, x2, y2, options),
-            ],
-          );
         }
         break;
       }

+ 6 - 0
src/scene/comparisons.ts

@@ -20,6 +20,12 @@ export const hasStroke = (type: string) =>
   type === "draw" ||
   type === "line";
 
+export const canChangeSharpness = (type: string) =>
+  type === "rectangle" ||
+  type === "arrow" ||
+  type === "draw" ||
+  type === "line";
+
 export const hasText = (type: string) => type === "text";
 
 export const getElementAtPosition = (

+ 1 - 0
src/scene/export.ts

@@ -165,5 +165,6 @@ const getWatermarkElement = (maxX: number, maxY: number) => {
     strokeStyle: "solid",
     roughness: 1,
     opacity: 100,
+    strokeSharpness: "sharp",
   });
 };

+ 1 - 0
src/scene/index.ts

@@ -10,6 +10,7 @@ export { normalizeScroll, calculateScrollCenter } from "./scroll";
 export {
   hasBackground,
   hasStroke,
+  canChangeSharpness,
   getElementAtPosition,
   getElementContainingPosition,
   hasText,

+ 5 - 0
src/tests/__snapshots__/dragCreate.test.tsx.snap

@@ -29,6 +29,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
@@ -56,6 +57,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "diamond",
@@ -83,6 +85,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "ellipse",
@@ -121,6 +124,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
@@ -148,6 +152,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",

+ 3 - 0
src/tests/__snapshots__/move.test.tsx.snap

@@ -14,6 +14,7 @@ Object {
   "roughness": 1,
   "seed": 401146281,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
@@ -39,6 +40,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
@@ -64,6 +66,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",

+ 2 - 0
src/tests/__snapshots__/multiPointCreate.test.tsx.snap

@@ -34,6 +34,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
@@ -79,6 +80,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",

Dosya farkı çok büyük olduğundan ihmal edildi
+ 131 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap


+ 2 - 0
src/tests/__snapshots__/resize.test.tsx.snap

@@ -14,6 +14,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
@@ -39,6 +40,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",

+ 5 - 0
src/tests/__snapshots__/selection.test.tsx.snap

@@ -27,6 +27,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
@@ -65,6 +66,7 @@ Object {
   "seed": 337897,
   "startBinding": null,
   "strokeColor": "#000000",
+  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
@@ -90,6 +92,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "diamond",
@@ -115,6 +118,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "ellipse",
@@ -140,6 +144,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",

+ 1 - 0
src/tests/zindex.test.tsx

@@ -37,6 +37,7 @@ const populateElements = (
       fillStyle: h.state.currentItemFillStyle,
       strokeWidth: h.state.currentItemStrokeWidth,
       strokeStyle: h.state.currentItemStrokeStyle,
+      strokeSharpness: h.state.currentItemStrokeSharpness,
       roughness: h.state.currentItemRoughness,
       opacity: h.state.currentItemOpacity,
     });

+ 2 - 0
src/types.ts

@@ -56,6 +56,8 @@ export type AppState = {
   currentItemFontFamily: FontFamily;
   currentItemFontSize: number;
   currentItemTextAlign: TextAlign;
+  currentItemStrokeSharpness: ExcalidrawElement["strokeSharpness"];
+  currentItemLinearStrokeSharpness: ExcalidrawElement["strokeSharpness"];
   viewBackgroundColor: string;
   scrollX: FlooredNumber;
   scrollY: FlooredNumber;

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor