فهرست منبع

feat: better default radius sizes for rectangles (#5553)

Co-authored-by: Ryan <diweihao@bytedance.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
Ryan Di 2 سال پیش
والد
کامیت
5854ac3eed
39فایلهای تغییر یافته به همراه814 افزوده شده و 380 حذف شده
  1. 2 10
      src/actions/actionFlip.ts
  2. 55 52
      src/actions/actionProperties.tsx
  3. 1 0
      src/actions/actionStyles.ts
  4. 1 1
      src/actions/types.ts
  5. 2 4
      src/appState.ts
  6. 2 2
      src/charts.ts
  7. 4 4
      src/components/Actions.tsx
  8. 18 6
      src/components/App.tsx
  9. 26 0
      src/constants.ts
  10. 16 4
      src/data/restore.ts
  11. 2 0
      src/element/bounds.test.ts
  12. 8 13
      src/element/bounds.ts
  13. 9 3
      src/element/collision.ts
  14. 4 12
      src/element/linearElementEditor.ts
  15. 3 3
      src/element/newElement.test.ts
  16. 4 3
      src/element/newElement.ts
  17. 2 0
      src/element/typeChecks.ts
  18. 10 3
      src/element/types.ts
  19. 34 2
      src/math.ts
  20. 6 0
      src/packages/excalidraw/CHANGELOG.md
  21. 5 2
      src/packages/excalidraw/example/App.tsx
  22. 1 1
      src/packages/utils/README.md
  23. 30 26
      src/renderer/renderElement.ts
  24. 1 1
      src/scene/comparisons.ts
  25. 1 1
      src/scene/index.ts
  26. 209 88
      src/tests/__snapshots__/contextmenu.test.tsx.snap
  27. 15 5
      src/tests/__snapshots__/dragCreate.test.tsx.snap
  28. 18 6
      src/tests/__snapshots__/move.test.tsx.snap
  29. 6 2
      src/tests/__snapshots__/multiPointCreate.test.tsx.snap
  30. 215 85
      src/tests/__snapshots__/regressionTests.test.tsx.snap
  31. 15 5
      src/tests/__snapshots__/selection.test.tsx.snap
  32. 27 9
      src/tests/data/__snapshots__/restore.test.ts.snap
  33. 2 2
      src/tests/data/restore.test.ts
  34. 1 1
      src/tests/fixtures/elementFixture.ts
  35. 28 5
      src/tests/helpers/api.ts
  36. 27 14
      src/tests/linearElementEditor.test.tsx
  37. 1 2
      src/tests/packages/__snapshots__/utils.test.ts.snap
  38. 1 1
      src/tests/scene/__snapshots__/export.test.ts.snap
  39. 2 2
      src/types.ts

+ 2 - 10
src/actions/actionFlip.ts

@@ -153,11 +153,7 @@ const flipElement = (
 
 
   let initialPointsCoords;
   let initialPointsCoords;
   if (isLinearElement(element)) {
   if (isLinearElement(element)) {
-    initialPointsCoords = getElementPointsCoords(
-      element,
-      element.points,
-      element.strokeSharpness,
-    );
+    initialPointsCoords = getElementPointsCoords(element, element.points);
   }
   }
   const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
   const initialElementAbsoluteCoords = getElementAbsoluteCoords(element);
 
 
@@ -215,11 +211,7 @@ const flipElement = (
     // Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
     // Adjusting origin because when a beizer curve path exceeds min/max points it offsets the origin.
     // There's still room for improvement since when the line roughness is > 1
     // There's still room for improvement since when the line roughness is > 1
     // we still have a small offset of the origin when fliipping the element.
     // we still have a small offset of the origin when fliipping the element.
-    const finalPointsCoords = getElementPointsCoords(
-      element,
-      element.points,
-      element.strokeSharpness,
-    );
+    const finalPointsCoords = getElementPointsCoords(element, element.points);
 
 
     const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
     const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
     const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];
     const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];

+ 55 - 52
src/actions/actionProperties.tsx

@@ -42,6 +42,7 @@ import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
   DEFAULT_FONT_SIZE,
   FONT_FAMILY,
   FONT_FAMILY,
+  ROUNDNESS,
   VERTICAL_ALIGN,
   VERTICAL_ALIGN,
 } from "../constants";
 } from "../constants";
 import {
 import {
@@ -57,7 +58,7 @@ import {
 import {
 import {
   isBoundToContainer,
   isBoundToContainer,
   isLinearElement,
   isLinearElement,
-  isLinearElementType,
+  isUsingAdaptiveRadius,
 } from "../element/typeChecks";
 } from "../element/typeChecks";
 import {
 import {
   Arrowhead,
   Arrowhead,
@@ -72,7 +73,7 @@ import { getLanguage, t } from "../i18n";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
 import { randomInteger } from "../random";
 import { randomInteger } from "../random";
 import {
 import {
-  canChangeSharpness,
+  canChangeRoundness,
   canHaveArrowheads,
   canHaveArrowheads,
   getCommonAttributeOfSelectedElements,
   getCommonAttributeOfSelectedElements,
   getSelectedElements,
   getSelectedElements,
@@ -848,69 +849,71 @@ export const actionChangeVerticalAlign = register({
   },
   },
 });
 });
 
 
-export const actionChangeSharpness = register({
-  name: "changeSharpness",
+export const actionChangeRoundness = register({
+  name: "changeRoundness",
   trackEvent: false,
   trackEvent: false,
   perform: (elements, appState, value) => {
   perform: (elements, appState, value) => {
-    const targetElements = getTargetElements(
-      getNonDeletedElements(elements),
-      appState,
-    );
-    const shouldUpdateForNonLinearElements = targetElements.length
-      ? targetElements.every((el) => !isLinearElement(el))
-      : !isLinearElementType(appState.activeTool.type);
-    const shouldUpdateForLinearElements = targetElements.length
-      ? targetElements.every(isLinearElement)
-      : isLinearElementType(appState.activeTool.type);
     return {
     return {
       elements: changeProperty(elements, appState, (el) =>
       elements: changeProperty(elements, appState, (el) =>
         newElementWith(el, {
         newElementWith(el, {
-          strokeSharpness: value,
+          roundness:
+            value === "round"
+              ? {
+                  type: isUsingAdaptiveRadius(el.type)
+                    ? ROUNDNESS.ADAPTIVE_RADIUS
+                    : ROUNDNESS.PROPORTIONAL_RADIUS,
+                }
+              : null,
         }),
         }),
       ),
       ),
       appState: {
       appState: {
         ...appState,
         ...appState,
-        currentItemStrokeSharpness: shouldUpdateForNonLinearElements
-          ? value
-          : appState.currentItemStrokeSharpness,
-        currentItemLinearStrokeSharpness: shouldUpdateForLinearElements
-          ? value
-          : appState.currentItemLinearStrokeSharpness,
+        currentItemRoundness: value,
       },
       },
       commitToHistory: true,
       commitToHistory: true,
     };
     };
   },
   },
-  PanelComponent: ({ elements, appState, updateData }) => (
-    <fieldset>
-      <legend>{t("labels.edges")}</legend>
-      <ButtonIconSelect
-        group="edges"
-        options={[
-          {
-            value: "sharp",
-            text: t("labels.sharp"),
-            icon: EdgeSharpIcon,
-          },
-          {
-            value: "round",
-            text: t("labels.round"),
-            icon: EdgeRoundIcon,
-          },
-        ]}
-        value={getFormValue(
-          elements,
-          appState,
-          (element) => element.strokeSharpness,
-          (canChangeSharpness(appState.activeTool.type) &&
-            (isLinearElementType(appState.activeTool.type)
-              ? appState.currentItemLinearStrokeSharpness
-              : appState.currentItemStrokeSharpness)) ||
-            null,
-        )}
-        onChange={(value) => updateData(value)}
-      />
-    </fieldset>
-  ),
+  PanelComponent: ({ elements, appState, updateData }) => {
+    const targetElements = getTargetElements(
+      getNonDeletedElements(elements),
+      appState,
+    );
+
+    const hasLegacyRoundness = targetElements.some(
+      (el) => el.roundness?.type === ROUNDNESS.LEGACY,
+    );
+
+    return (
+      <fieldset>
+        <legend>{t("labels.edges")}</legend>
+        <ButtonIconSelect
+          group="edges"
+          options={[
+            {
+              value: "sharp",
+              text: t("labels.sharp"),
+              icon: EdgeSharpIcon,
+            },
+            {
+              value: "round",
+              text: t("labels.round"),
+              icon: EdgeRoundIcon,
+            },
+          ]}
+          value={getFormValue(
+            elements,
+            appState,
+            (element) =>
+              hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
+            (canChangeRoundness(appState.activeTool.type) &&
+              appState.currentItemRoundness) ||
+              null,
+          )}
+          onChange={(value) => updateData(value)}
+        />
+      </fieldset>
+    );
+  },
 });
 });
 
 
 export const actionChangeArrowhead = register({
 export const actionChangeArrowhead = register({

+ 1 - 0
src/actions/actionStyles.ts

@@ -77,6 +77,7 @@ export const actionPasteStyles = register({
             fillStyle: elementStylesToCopyFrom?.fillStyle,
             fillStyle: elementStylesToCopyFrom?.fillStyle,
             opacity: elementStylesToCopyFrom?.opacity,
             opacity: elementStylesToCopyFrom?.opacity,
             roughness: elementStylesToCopyFrom?.roughness,
             roughness: elementStylesToCopyFrom?.roughness,
+            roundness: elementStylesToCopyFrom?.roundness,
           });
           });
 
 
           if (isTextElement(newElement)) {
           if (isTextElement(newElement)) {

+ 1 - 1
src/actions/types.ts

@@ -91,7 +91,7 @@ export type ActionName =
   | "ungroup"
   | "ungroup"
   | "goToCollaborator"
   | "goToCollaborator"
   | "addToLibrary"
   | "addToLibrary"
-  | "changeSharpness"
+  | "changeRoundness"
   | "alignTop"
   | "alignTop"
   | "alignBottom"
   | "alignBottom"
   | "alignLeft"
   | "alignLeft"

+ 2 - 4
src/appState.ts

@@ -28,12 +28,11 @@ export const getDefaultAppState = (): Omit<
     currentItemFillStyle: "hachure",
     currentItemFillStyle: "hachure",
     currentItemFontFamily: DEFAULT_FONT_FAMILY,
     currentItemFontFamily: DEFAULT_FONT_FAMILY,
     currentItemFontSize: DEFAULT_FONT_SIZE,
     currentItemFontSize: DEFAULT_FONT_SIZE,
-    currentItemLinearStrokeSharpness: "round",
     currentItemOpacity: 100,
     currentItemOpacity: 100,
     currentItemRoughness: 1,
     currentItemRoughness: 1,
     currentItemStartArrowhead: null,
     currentItemStartArrowhead: null,
     currentItemStrokeColor: oc.black,
     currentItemStrokeColor: oc.black,
-    currentItemStrokeSharpness: "sharp",
+    currentItemRoundness: "round",
     currentItemStrokeStyle: "solid",
     currentItemStrokeStyle: "solid",
     currentItemStrokeWidth: 1,
     currentItemStrokeWidth: 1,
     currentItemTextAlign: DEFAULT_TEXT_ALIGN,
     currentItemTextAlign: DEFAULT_TEXT_ALIGN,
@@ -120,7 +119,7 @@ const APP_STATE_STORAGE_CONF = (<
   currentItemFillStyle: { browser: true, export: false, server: false },
   currentItemFillStyle: { browser: true, export: false, server: false },
   currentItemFontFamily: { browser: true, export: false, server: false },
   currentItemFontFamily: { browser: true, export: false, server: false },
   currentItemFontSize: { browser: true, export: false, server: false },
   currentItemFontSize: { browser: true, export: false, server: false },
-  currentItemLinearStrokeSharpness: {
+  currentItemRoundness: {
     browser: true,
     browser: true,
     export: false,
     export: false,
     server: false,
     server: false,
@@ -129,7 +128,6 @@ const APP_STATE_STORAGE_CONF = (<
   currentItemRoughness: { browser: true, export: false, server: false },
   currentItemRoughness: { browser: true, export: false, server: false },
   currentItemStartArrowhead: { browser: true, export: false, server: false },
   currentItemStartArrowhead: { browser: true, export: false, server: false },
   currentItemStrokeColor: { browser: true, export: false, server: false },
   currentItemStrokeColor: { browser: true, export: false, server: false },
-  currentItemStrokeSharpness: { browser: true, export: false, server: false },
   currentItemStrokeStyle: { browser: true, export: false, server: false },
   currentItemStrokeStyle: { browser: true, export: false, server: false },
   currentItemStrokeWidth: { browser: true, export: false, server: false },
   currentItemStrokeWidth: { browser: true, export: false, server: false },
   currentItemTextAlign: { browser: true, export: false, server: false },
   currentItemTextAlign: { browser: true, export: false, server: false },

+ 2 - 2
src/charts.ts

@@ -172,7 +172,7 @@ const commonProps = {
   opacity: 100,
   opacity: 100,
   roughness: 1,
   roughness: 1,
   strokeColor: colors.elementStroke[0],
   strokeColor: colors.elementStroke[0],
-  strokeSharpness: "sharp",
+  roundness: null,
   strokeStyle: "solid",
   strokeStyle: "solid",
   strokeWidth: 1,
   strokeWidth: 1,
   verticalAlign: VERTICAL_ALIGN.MIDDLE,
   verticalAlign: VERTICAL_ALIGN.MIDDLE,
@@ -322,7 +322,7 @@ const chartBaseElements = (
         text: spreadsheet.title,
         text: spreadsheet.title,
         x: x + chartWidth / 2,
         x: x + chartWidth / 2,
         y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
         y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
-        strokeSharpness: "sharp",
+        roundness: null,
         strokeStyle: "solid",
         strokeStyle: "solid",
         textAlign: "center",
         textAlign: "center",
       })
       })

+ 4 - 4
src/components/Actions.tsx

@@ -5,7 +5,7 @@ import { ExcalidrawElement, PointerType } from "../element/types";
 import { t } from "../i18n";
 import { t } from "../i18n";
 import { useDevice } from "../components/App";
 import { useDevice } from "../components/App";
 import {
 import {
-  canChangeSharpness,
+  canChangeRoundness,
   canHaveArrowheads,
   canHaveArrowheads,
   getTargetElements,
   getTargetElements,
   hasBackground,
   hasBackground,
@@ -110,9 +110,9 @@ export const SelectedShapeActions = ({
         </>
         </>
       )}
       )}
 
 
-      {(canChangeSharpness(appState.activeTool.type) ||
-        targetElements.some((element) => canChangeSharpness(element.type))) && (
-        <>{renderAction("changeSharpness")}</>
+      {(canChangeRoundness(appState.activeTool.type) ||
+        targetElements.some((element) => canChangeRoundness(element.type))) && (
+        <>{renderAction("changeRoundness")}</>
       )}
       )}
 
 
       {(hasText(appState.activeTool.type) ||
       {(hasText(appState.activeTool.type) ||

+ 18 - 6
src/components/App.tsx

@@ -70,6 +70,7 @@ import {
   MQ_RIGHT_SIDEBAR_MIN_WIDTH,
   MQ_RIGHT_SIDEBAR_MIN_WIDTH,
   MQ_SM_MAX_WIDTH,
   MQ_SM_MAX_WIDTH,
   POINTER_BUTTON,
   POINTER_BUTTON,
+  ROUNDNESS,
   SCROLL_TIMEOUT,
   SCROLL_TIMEOUT,
   TAP_TWICE_TIMEOUT,
   TAP_TWICE_TIMEOUT,
   TEXT_TO_CENTER_SNAP_THRESHOLD,
   TEXT_TO_CENTER_SNAP_THRESHOLD,
@@ -134,6 +135,7 @@ import {
   isInitializedImageElement,
   isInitializedImageElement,
   isLinearElement,
   isLinearElement,
   isLinearElementType,
   isLinearElementType,
+  isUsingAdaptiveRadius,
 } from "../element/typeChecks";
 } from "../element/typeChecks";
 import {
 import {
   ExcalidrawBindableElement,
   ExcalidrawBindableElement,
@@ -1658,9 +1660,9 @@ class App extends React.Component<AppProps, AppState> {
       fillStyle: this.state.currentItemFillStyle,
       fillStyle: this.state.currentItemFillStyle,
       strokeWidth: this.state.currentItemStrokeWidth,
       strokeWidth: this.state.currentItemStrokeWidth,
       strokeStyle: this.state.currentItemStrokeStyle,
       strokeStyle: this.state.currentItemStrokeStyle,
+      roundness: null,
       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,
@@ -2569,7 +2571,7 @@ class App extends React.Component<AppProps, 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,
+          roundness: null,
           text: "",
           text: "",
           fontSize: this.state.currentItemFontSize,
           fontSize: this.state.currentItemFontSize,
           fontFamily: this.state.currentItemFontFamily,
           fontFamily: this.state.currentItemFontFamily,
@@ -4072,7 +4074,7 @@ class App extends React.Component<AppProps, 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,
+      roundness: null,
       simulatePressure: event.pressure === 0.5,
       simulatePressure: event.pressure === 0.5,
       locked: false,
       locked: false,
     });
     });
@@ -4128,8 +4130,8 @@ class App extends React.Component<AppProps, AppState> {
       strokeWidth: this.state.currentItemStrokeWidth,
       strokeWidth: this.state.currentItemStrokeWidth,
       strokeStyle: this.state.currentItemStrokeStyle,
       strokeStyle: this.state.currentItemStrokeStyle,
       roughness: this.state.currentItemRoughness,
       roughness: this.state.currentItemRoughness,
+      roundness: null,
       opacity: this.state.currentItemOpacity,
       opacity: this.state.currentItemOpacity,
-      strokeSharpness: this.state.currentItemLinearStrokeSharpness,
       locked: false,
       locked: false,
     });
     });
 
 
@@ -4215,7 +4217,10 @@ class App extends React.Component<AppProps, 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,
+        roundness:
+          this.state.currentItemRoundness === "round"
+            ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
+            : null,
         startArrowhead,
         startArrowhead,
         endArrowhead,
         endArrowhead,
         locked: false,
         locked: false,
@@ -4266,7 +4271,14 @@ class App extends React.Component<AppProps, 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,
+      roundness:
+        this.state.currentItemRoundness === "round"
+          ? {
+              type: isUsingAdaptiveRadius(elementType)
+                ? ROUNDNESS.ADAPTIVE_RADIUS
+                : ROUNDNESS.PROPORTIONAL_RADIUS,
+            }
+          : null,
       locked: false,
       locked: false,
     });
     });
 
 

+ 26 - 0
src/constants.ts

@@ -216,6 +216,32 @@ export const TEXT_ALIGN = {
 
 
 export const ELEMENT_READY_TO_ERASE_OPACITY = 20;
 export const ELEMENT_READY_TO_ERASE_OPACITY = 20;
 
 
+// Radius represented as 25% of element's largest side (width/height).
+// Used for LEGACY and PROPORTIONAL_RADIUS algorithms, or when the element is
+// below the cutoff size.
+export const DEFAULT_PROPORTIONAL_RADIUS = 0.25;
+// Fixed radius for the ADAPTIVE_RADIUS algorithm. In pixels.
+export const DEFAULT_ADAPTIVE_RADIUS = 32;
+// roundness type (algorithm)
+export const ROUNDNESS = {
+  // Used for legacy rounding (rectangles), which currently works the same
+  // as PROPORTIONAL_RADIUS, but we need to differentiate for UI purposes and
+  // forwards-compat.
+  LEGACY: 1,
+
+  // Used for linear elements & diamonds
+  PROPORTIONAL_RADIUS: 2,
+
+  // Current default algorithm for rectangles, using fixed pixel radius.
+  // It's working similarly to a regular border-radius, but attemps to make
+  // radius visually similar across differnt element sizes, especially
+  // very large and very small elements.
+  //
+  // NOTE right now we don't allow configuration and use a constant radius
+  // (see DEFAULT_ADAPTIVE_RADIUS constant)
+  ADAPTIVE_RADIUS: 3,
+} as const;
+
 export const COOKIES = {
 export const COOKIES = {
   AUTH_STATE_COOKIE: "excplus-auth",
   AUTH_STATE_COOKIE: "excplus-auth",
 } as const;
 } as const;

+ 16 - 4
src/data/restore.ts

@@ -3,6 +3,7 @@ import {
   ExcalidrawSelectionElement,
   ExcalidrawSelectionElement,
   ExcalidrawTextElement,
   ExcalidrawTextElement,
   FontFamilyValues,
   FontFamilyValues,
+  StrokeRoundness,
 } from "../element/types";
 } from "../element/types";
 import {
 import {
   AppState,
   AppState,
@@ -17,7 +18,7 @@ import {
   isInvisiblySmallElement,
   isInvisiblySmallElement,
   refreshTextDimensions,
   refreshTextDimensions,
 } from "../element";
 } from "../element";
-import { isLinearElementType, isTextElement } from "../element/typeChecks";
+import { isTextElement, isUsingAdaptiveRadius } from "../element/typeChecks";
 import { randomId } from "../random";
 import { randomId } from "../random";
 import {
 import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_FAMILY,
@@ -25,6 +26,7 @@ import {
   DEFAULT_VERTICAL_ALIGN,
   DEFAULT_VERTICAL_ALIGN,
   PRECEDING_ELEMENT_KEY,
   PRECEDING_ELEMENT_KEY,
   FONT_FAMILY,
   FONT_FAMILY,
+  ROUNDNESS,
 } from "../constants";
 } from "../constants";
 import { getDefaultAppState } from "../appState";
 import { getDefaultAppState } from "../appState";
 import { LinearElementEditor } from "../element/linearElementEditor";
 import { LinearElementEditor } from "../element/linearElementEditor";
@@ -74,6 +76,8 @@ const restoreElementWithProperties = <
     customData?: ExcalidrawElement["customData"];
     customData?: ExcalidrawElement["customData"];
     /** @deprecated */
     /** @deprecated */
     boundElementIds?: readonly ExcalidrawElement["id"][];
     boundElementIds?: readonly ExcalidrawElement["id"][];
+    /** @deprecated */
+    strokeSharpness?: StrokeRoundness;
     /** metadata that may be present in elements during collaboration */
     /** metadata that may be present in elements during collaboration */
     [PRECEDING_ELEMENT_KEY]?: string;
     [PRECEDING_ELEMENT_KEY]?: string;
   },
   },
@@ -112,9 +116,17 @@ const restoreElementWithProperties = <
     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"),
+    roundness: element.roundness
+      ? element.roundness
+      : element.strokeSharpness === "round"
+      ? {
+          // for old elements that would now use adaptive radius algo,
+          // use legacy algo instead
+          type: isUsingAdaptiveRadius(element.type)
+            ? ROUNDNESS.LEGACY
+            : ROUNDNESS.PROPORTIONAL_RADIUS,
+        }
+      : null,
     boundElements: element.boundElementIds
     boundElements: element.boundElementIds
       ? element.boundElementIds.map((id) => ({ type: "arrow", id }))
       ? element.boundElementIds.map((id) => ({ type: "arrow", id }))
       : element.boundElements ?? [],
       : element.boundElements ?? [],

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

@@ -1,3 +1,4 @@
+import { ROUNDNESS } from "../constants";
 import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
 import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
 import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
 import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
 
 
@@ -22,6 +23,7 @@ const _ce = ({
     backgroundColor: "#000",
     backgroundColor: "#000",
     fillStyle: "solid",
     fillStyle: "solid",
     strokeWidth: 1,
     strokeWidth: 1,
+    roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
     roughness: 0,
     roughness: 0,
     opacity: 1,
     opacity: 1,
     x,
     x,

+ 8 - 13
src/element/bounds.ts

@@ -378,7 +378,7 @@ const generateLinearElementShape = (
   const options = generateRoughOptions(element);
   const options = generateRoughOptions(element);
 
 
   const method = (() => {
   const method = (() => {
-    if (element.strokeSharpness !== "sharp") {
+    if (element.roundness) {
       return "curve";
       return "curve";
     }
     }
     if (options.fill) {
     if (options.fill) {
@@ -561,16 +561,12 @@ export const getResizedElementAbsoluteCoords = (
   } else {
   } else {
     // Line
     // Line
     const gen = rough.generator();
     const gen = rough.generator();
-    const curve =
-      element.strokeSharpness === "sharp"
-        ? gen.linearPath(
-            points as [number, number][],
-            generateRoughOptions(element),
-          )
-        : gen.curve(
-            points as [number, number][],
-            generateRoughOptions(element),
-          );
+    const curve = !element.roundness
+      ? gen.linearPath(
+          points as [number, number][],
+          generateRoughOptions(element),
+        )
+      : gen.curve(points as [number, number][], generateRoughOptions(element));
 
 
     const ops = getCurvePathOps(curve);
     const ops = getCurvePathOps(curve);
     bounds = getMinMaxXYFromCurvePathOps(ops);
     bounds = getMinMaxXYFromCurvePathOps(ops);
@@ -588,12 +584,11 @@ 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 =
   const curve =
-    sharpness === "sharp"
+    element.roundness == null
       ? gen.linearPath(
       ? gen.linearPath(
           points as [number, number][],
           points as [number, number][],
           generateRoughOptions(element),
           generateRoughOptions(element),

+ 9 - 3
src/element/collision.ts

@@ -25,6 +25,7 @@ import {
   ExcalidrawFreeDrawElement,
   ExcalidrawFreeDrawElement,
   ExcalidrawImageElement,
   ExcalidrawImageElement,
   ExcalidrawLinearElement,
   ExcalidrawLinearElement,
+  StrokeRoundness,
 } from "./types";
 } from "./types";
 
 
 import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
 import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@@ -419,7 +420,12 @@ 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, element.strokeSharpness),
+      hitTestCurveInside(
+        subshape,
+        relX,
+        relY,
+        element.roundness ? "round" : "sharp",
+      ),
     );
     );
     if (hit) {
     if (hit) {
       return true;
       return true;
@@ -851,7 +857,7 @@ const hitTestCurveInside = (
   drawable: Drawable,
   drawable: Drawable,
   x: number,
   x: number,
   y: number,
   y: number,
-  sharpness: ExcalidrawElement["strokeSharpness"],
+  roundness: StrokeRoundness,
 ) => {
 ) => {
   const ops = getCurvePathOps(drawable);
   const ops = getCurvePathOps(drawable);
   const points: Mutable<Point>[] = [];
   const points: Mutable<Point>[] = [];
@@ -875,7 +881,7 @@ const hitTestCurveInside = (
     }
     }
   }
   }
   if (points.length >= 4) {
   if (points.length >= 4) {
-    if (sharpness === "sharp") {
+    if (roundness === "sharp") {
       return isPointInPolygon(points, x, y);
       return isPointInPolygon(points, x, y);
     }
     }
     const polygonPoints = pointsOnBezierCurves(points, 10, 5);
     const polygonPoints = pointsOnBezierCurves(points, 10, 5);

+ 4 - 12
src/element/linearElementEditor.ts

@@ -527,7 +527,7 @@ export class LinearElementEditor {
       endPoint[0],
       endPoint[0],
       endPoint[1],
       endPoint[1],
     );
     );
-    if (element.points.length > 2 && element.strokeSharpness === "round") {
+    if (element.points.length > 2 && element.roundness) {
       distance = getBezierCurveLength(element, endPoint);
       distance = getBezierCurveLength(element, endPoint);
     }
     }
 
 
@@ -541,7 +541,7 @@ export class LinearElementEditor {
     endPointIndex: number,
     endPointIndex: number,
   ) {
   ) {
     let segmentMidPoint = centerPoint(startPoint, endPoint);
     let segmentMidPoint = centerPoint(startPoint, endPoint);
-    if (element.points.length > 2 && element.strokeSharpness === "round") {
+    if (element.points.length > 2 && element.roundness) {
       const controlPoints = getControlPointsForBezierCurve(
       const controlPoints = getControlPointsForBezierCurve(
         element,
         element,
         element.points[endPointIndex],
         element.points[endPointIndex],
@@ -1221,16 +1221,8 @@ export class LinearElementEditor {
     offsetY: number,
     offsetY: number,
     otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
     otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
   ) {
   ) {
-    const nextCoords = getElementPointsCoords(
-      element,
-      nextPoints,
-      element.strokeSharpness || "round",
-    );
-    const prevCoords = getElementPointsCoords(
-      element,
-      element.points,
-      element.strokeSharpness || "round",
-    );
+    const nextCoords = getElementPointsCoords(element, nextPoints);
+    const prevCoords = getElementPointsCoords(element, element.points);
     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;

+ 3 - 3
src/element/newElement.test.ts

@@ -1,7 +1,7 @@
 import { duplicateElement } from "./newElement";
 import { duplicateElement } from "./newElement";
 import { mutateElement } from "./mutateElement";
 import { mutateElement } from "./mutateElement";
 import { API } from "../tests/helpers/api";
 import { API } from "../tests/helpers/api";
-import { FONT_FAMILY } from "../constants";
+import { FONT_FAMILY, ROUNDNESS } from "../constants";
 import { isPrimitive } from "../utils";
 import { isPrimitive } from "../utils";
 
 
 const assertCloneObjects = (source: any, clone: any) => {
 const assertCloneObjects = (source: any, clone: any) => {
@@ -25,7 +25,7 @@ it("clones arrow element", () => {
     fillStyle: "hachure",
     fillStyle: "hachure",
     strokeWidth: 1,
     strokeWidth: 1,
     strokeStyle: "solid",
     strokeStyle: "solid",
-    strokeSharpness: "round",
+    roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
     roughness: 1,
     roughness: 1,
     opacity: 100,
     opacity: 100,
   });
   });
@@ -71,7 +71,7 @@ it("clones text element", () => {
     fillStyle: "hachure",
     fillStyle: "hachure",
     strokeWidth: 1,
     strokeWidth: 1,
     strokeStyle: "solid",
     strokeStyle: "solid",
-    strokeSharpness: "round",
+    roundness: null,
     roughness: 1,
     roughness: 1,
     opacity: 100,
     opacity: 100,
     text: "hello",
     text: "hello",

+ 4 - 3
src/element/newElement.ts

@@ -62,14 +62,15 @@ const _newElementBase = <T extends ExcalidrawElement>(
     height = 0,
     height = 0,
     angle = 0,
     angle = 0,
     groupIds = [],
     groupIds = [],
-    strokeSharpness,
+    roundness = null,
     boundElements = null,
     boundElements = null,
     link = null,
     link = null,
     locked,
     locked,
     ...rest
     ...rest
   }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
   }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
 ) => {
 ) => {
-  const element = {
+  // assign type to guard against excess properties
+  const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
     id: rest.id || randomId(),
     id: rest.id || randomId(),
     type,
     type,
     x,
     x,
@@ -85,7 +86,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
     roughness,
     roughness,
     opacity,
     opacity,
     groupIds,
     groupIds,
-    strokeSharpness,
+    roundness,
     seed: rest.seed ?? randomInteger(),
     seed: rest.seed ?? randomInteger(),
     version: rest.version || 1,
     version: rest.version || 1,
     versionNonce: rest.versionNonce ?? 0,
     versionNonce: rest.versionNonce ?? 0,

+ 2 - 0
src/element/typeChecks.ts

@@ -152,3 +152,5 @@ export const isBoundToContainer = (
     isTextElement(element)
     isTextElement(element)
   );
   );
 };
 };
+
+export const isUsingAdaptiveRadius = (type: string) => type === "rectangle";

+ 10 - 3
src/element/types.ts

@@ -1,5 +1,11 @@
 import { Point } from "../types";
 import { Point } from "../types";
-import { FONT_FAMILY, TEXT_ALIGN, THEME, VERTICAL_ALIGN } from "../constants";
+import {
+  FONT_FAMILY,
+  ROUNDNESS,
+  TEXT_ALIGN,
+  THEME,
+  VERTICAL_ALIGN,
+} from "../constants";
 
 
 export type ChartType = "bar" | "line";
 export type ChartType = "bar" | "line";
 export type FillStyle = "hachure" | "cross-hatch" | "solid";
 export type FillStyle = "hachure" | "cross-hatch" | "solid";
@@ -9,7 +15,8 @@ export type Theme = typeof THEME[keyof typeof THEME];
 export type FontString = string & { _brand: "fontString" };
 export type FontString = string & { _brand: "fontString" };
 export type GroupId = string;
 export type GroupId = string;
 export type PointerType = "mouse" | "pen" | "touch";
 export type PointerType = "mouse" | "pen" | "touch";
-export type StrokeSharpness = "round" | "sharp";
+export type StrokeRoundness = "round" | "sharp";
+export type RoundnessType = ValueOf<typeof ROUNDNESS>;
 export type StrokeStyle = "solid" | "dashed" | "dotted";
 export type StrokeStyle = "solid" | "dashed" | "dotted";
 export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
 export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
 
 
@@ -25,7 +32,7 @@ type _ExcalidrawElementBase = Readonly<{
   fillStyle: FillStyle;
   fillStyle: FillStyle;
   strokeWidth: number;
   strokeWidth: number;
   strokeStyle: StrokeStyle;
   strokeStyle: StrokeStyle;
-  strokeSharpness: StrokeSharpness;
+  roundness: null | { type: RoundnessType; value?: number };
   roughness: number;
   roughness: number;
   opacity: number;
   opacity: number;
   width: number;
   width: number;

+ 34 - 2
src/math.ts

@@ -1,6 +1,15 @@
 import { NormalizedZoomValue, Point, Zoom } from "./types";
 import { NormalizedZoomValue, Point, Zoom } from "./types";
-import { LINE_CONFIRM_THRESHOLD } from "./constants";
-import { ExcalidrawLinearElement, NonDeleted } from "./element/types";
+import {
+  DEFAULT_ADAPTIVE_RADIUS,
+  LINE_CONFIRM_THRESHOLD,
+  DEFAULT_PROPORTIONAL_RADIUS,
+  ROUNDNESS,
+} from "./constants";
+import {
+  ExcalidrawElement,
+  ExcalidrawLinearElement,
+  NonDeleted,
+} from "./element/types";
 import { getShapeForElement } from "./renderer/renderElement";
 import { getShapeForElement } from "./renderer/renderElement";
 import { getCurvePathOps } from "./element/bounds";
 import { getCurvePathOps } from "./element/bounds";
 
 
@@ -266,6 +275,29 @@ export const getGridPoint = (
   return [x, y];
   return [x, y];
 };
 };
 
 
+export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
+  if (
+    element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS ||
+    element.roundness?.type === ROUNDNESS.LEGACY
+  ) {
+    return x * DEFAULT_PROPORTIONAL_RADIUS;
+  }
+
+  if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) {
+    const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS;
+
+    const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS;
+
+    if (x <= CUTOFF_SIZE) {
+      return x * DEFAULT_PROPORTIONAL_RADIUS;
+    }
+
+    return fixedRadiusSize;
+  }
+
+  return 0;
+};
+
 export const getControlPointsForBezierCurve = (
 export const getControlPointsForBezierCurve = (
   element: NonDeleted<ExcalidrawLinearElement>,
   element: NonDeleted<ExcalidrawLinearElement>,
   endPoint: Point,
   endPoint: Point,

+ 6 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR
 Please add the latest change on the top under the correct section.
 Please add the latest change on the top under the correct section.
 -->
 -->
 
 
+## Unreleased
+
+### Excalidraw schema
+
+- Merged `appState.currentItemStrokeSharpness` and `appState.currentItemLinearStrokeSharpness` into `appState.currentItemRoundness`. Renamed `changeSharpness` action to `changeRoundness`. Excalidraw element's `strokeSharpness` was changed to `roundness`. Check the PR for types and more details [#5553](https://github.com/excalidraw/excalidraw/pull/5553).
+
 ## 0.13.0 (2022-10-27)
 ## 0.13.0 (2022-10-27)
 
 
 ### Excalidraw API
 ### Excalidraw API

+ 5 - 2
src/packages/excalidraw/example/App.tsx

@@ -13,7 +13,7 @@ import {
   withBatchedUpdates,
   withBatchedUpdates,
   withBatchedUpdatesThrottled,
   withBatchedUpdatesThrottled,
 } from "../../../utils";
 } from "../../../utils";
-import { EVENT } from "../../../constants";
+import { EVENT, ROUNDNESS } from "../../../constants";
 import { distance2d } from "../../../math";
 import { distance2d } from "../../../math";
 import { fileOpen } from "../../../data/filesystem";
 import { fileOpen } from "../../../data/filesystem";
 import { loadSceneOrLibraryFromBlob } from "../../utils";
 import { loadSceneOrLibraryFromBlob } from "../../utils";
@@ -244,7 +244,10 @@ export default function App() {
             locked: false,
             locked: false,
             link: null,
             link: null,
             updated: 1,
             updated: 1,
-            strokeSharpness: "round",
+            roundness: {
+              type: ROUNDNESS.ADAPTIVE_RADIUS,
+              value: 32,
+            },
           },
           },
         ],
         ],
         null,
         null,

+ 1 - 1
src/packages/utils/README.md

@@ -68,7 +68,7 @@ const excalidrawDiagram = {
       roughness: 1,
       roughness: 1,
       opacity: 100,
       opacity: 100,
       groupIds: [],
       groupIds: [],
-      strokeSharpness: "sharp",
+      roundness: null,
       seed: 1041657908,
       seed: 1041657908,
       version: 120,
       version: 120,
       versionNonce: 1188004276,
       versionNonce: 1188004276,

+ 30 - 26
src/renderer/renderElement.ts

@@ -27,7 +27,7 @@ import { RoughGenerator } from "roughjs/bin/generator";
 
 
 import { RenderConfig } from "../scene/types";
 import { RenderConfig } from "../scene/types";
 import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
 import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
-import { isPathALoop } from "../math";
+import { getCornerRadius, isPathALoop } from "../math";
 import rough from "roughjs/bin/rough";
 import rough from "roughjs/bin/rough";
 import { AppState, BinaryFiles, Zoom } from "../types";
 import { AppState, BinaryFiles, Zoom } from "../types";
 import { getDefaultAppState } from "../appState";
 import { getDefaultAppState } from "../appState";
@@ -424,10 +424,10 @@ const generateElementShape = (
 
 
     switch (element.type) {
     switch (element.type) {
       case "rectangle":
       case "rectangle":
-        if (element.strokeSharpness === "round") {
+        if (element.roundness) {
           const w = element.width;
           const w = element.width;
           const h = element.height;
           const h = element.height;
-          const r = Math.min(w, h) * 0.25;
+          const r = getCornerRadius(Math.min(w, h), element);
           shape = generator.path(
           shape = generator.path(
             `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${
             `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${
               h - r
               h - r
@@ -451,32 +451,36 @@ const generateElementShape = (
       case "diamond": {
       case "diamond": {
         const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
         const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
           getDiamondPoints(element);
           getDiamondPoints(element);
-        if (element.strokeSharpness === "round") {
+        if (element.roundness) {
+          const verticalRadius = getCornerRadius(
+            Math.abs(topX - leftX),
+            element,
+          );
+
+          const horizontalRadius = getCornerRadius(
+            Math.abs(rightY - topY),
+            element,
+          );
+
           shape = generator.path(
           shape = generator.path(
-            `M ${topX + (rightX - topX) * 0.25} ${
-              topY + (rightY - topY) * 0.25
-            } L ${rightX - (rightX - topX) * 0.25} ${
-              rightY - (rightY - topY) * 0.25
-            }
+            `M ${topX + verticalRadius} ${topY + horizontalRadius} L ${
+              rightX - verticalRadius
+            } ${rightY - horizontalRadius}
             C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
             C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
-              rightX - (rightX - bottomX) * 0.25
-            } ${rightY + (bottomY - rightY) * 0.25}
-            L ${bottomX + (rightX - bottomX) * 0.25} ${
-              bottomY - (bottomY - rightY) * 0.25
-            }
+              rightX - verticalRadius
+            } ${rightY + horizontalRadius}
+            L ${bottomX + verticalRadius} ${bottomY - horizontalRadius}
             C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
             C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
-              bottomX - (bottomX - leftX) * 0.25
-            } ${bottomY - (bottomY - leftY) * 0.25}
-            L ${leftX + (bottomX - leftX) * 0.25} ${
-              leftY + (bottomY - leftY) * 0.25
+              bottomX - verticalRadius
+            } ${bottomY - horizontalRadius}
+            L ${leftX + verticalRadius} ${leftY + horizontalRadius}
+            C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${
+              leftY - horizontalRadius
             }
             }
-            C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
-              leftX + (topX - leftX) * 0.25
-            } ${leftY - (leftY - topY) * 0.25}
-            L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
-            C ${topX} ${topY}, ${topX} ${topY}, ${
-              topX + (rightX - topX) * 0.25
-            } ${topY + (rightY - topY) * 0.25}`,
+            L ${topX - verticalRadius} ${topY + horizontalRadius}
+            C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
+              topY + horizontalRadius
+            }`,
             generateRoughOptions(element, true),
             generateRoughOptions(element, true),
           );
           );
         } else {
         } else {
@@ -515,7 +519,7 @@ 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 (!element.roundness) {
           if (options.fill) {
           if (options.fill) {
             shape = [generator.polygon(points as [number, number][], options)];
             shape = [generator.polygon(points as [number, number][], options)];
           } else {
           } else {

+ 1 - 1
src/scene/comparisons.ts

@@ -24,7 +24,7 @@ export const hasStrokeStyle = (type: string) =>
   type === "arrow" ||
   type === "arrow" ||
   type === "line";
   type === "line";
 
 
-export const canChangeSharpness = (type: string) =>
+export const canChangeRoundness = (type: string) =>
   type === "rectangle" ||
   type === "rectangle" ||
   type === "arrow" ||
   type === "arrow" ||
   type === "line" ||
   type === "line" ||

+ 1 - 1
src/scene/index.ts

@@ -12,7 +12,7 @@ export {
   hasStrokeWidth,
   hasStrokeWidth,
   hasStrokeStyle,
   hasStrokeStyle,
   canHaveArrowheads,
   canHaveArrowheads,
-  canChangeSharpness,
+  canChangeRoundness,
   getElementAtPosition,
   getElementAtPosition,
   hasText,
   hasText,
   getElementsAtPosition,
   getElementsAtPosition,

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 209 - 88
src/tests/__snapshots__/contextmenu.test.tsx.snap


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

@@ -29,11 +29,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "arrow",
   "type": "arrow",
@@ -62,9 +64,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "diamond",
   "type": "diamond",
@@ -93,9 +97,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "ellipse",
   "type": "ellipse",
@@ -135,11 +141,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",
@@ -168,9 +176,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",

+ 18 - 6
src/tests/__snapshots__/move.test.tsx.snap

@@ -14,9 +14,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 401146281,
   "seed": 401146281,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",
@@ -43,9 +45,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",
@@ -72,9 +76,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",
@@ -106,9 +112,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",
@@ -140,9 +148,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 449462985,
   "seed": 449462985,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",
@@ -186,6 +196,9 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 401146281,
   "seed": 401146281,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": Object {
   "startBinding": Object {
@@ -194,7 +207,6 @@ Object {
     "gap": 10,
     "gap": 10,
   },
   },
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",

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

@@ -34,11 +34,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "arrow",
   "type": "arrow",
@@ -85,11 +87,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 215 - 85
src/tests/__snapshots__/regressionTests.test.tsx.snap


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

@@ -27,11 +27,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "arrow",
   "type": "arrow",
@@ -71,11 +73,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",
@@ -102,9 +106,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "diamond",
   "type": "diamond",
@@ -131,9 +137,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "ellipse",
   "type": "ellipse",
@@ -160,9 +168,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 100,
   "opacity": 100,
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "seed": 337897,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "rectangle",
   "type": "rectangle",

+ 27 - 9
src/tests/data/__snapshots__/restore.test.ts.snap

@@ -27,11 +27,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "arrow",
   "type": "arrow",
@@ -62,9 +64,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 10,
   "opacity": 10,
   "roughness": 2,
   "roughness": 2,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "strokeColor": "red",
   "strokeColor": "red",
-  "strokeSharpness": "round",
   "strokeStyle": "dashed",
   "strokeStyle": "dashed",
   "strokeWidth": 2,
   "strokeWidth": 2,
   "type": "rectangle",
   "type": "rectangle",
@@ -95,9 +99,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 10,
   "opacity": 10,
   "roughness": 2,
   "roughness": 2,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "strokeColor": "red",
   "strokeColor": "red",
-  "strokeSharpness": "round",
   "strokeStyle": "dashed",
   "strokeStyle": "dashed",
   "strokeWidth": 2,
   "strokeWidth": 2,
   "type": "ellipse",
   "type": "ellipse",
@@ -128,9 +134,11 @@ Object {
   "locked": false,
   "locked": false,
   "opacity": 10,
   "opacity": 10,
   "roughness": 2,
   "roughness": 2,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "strokeColor": "red",
   "strokeColor": "red",
-  "strokeSharpness": "round",
   "strokeStyle": "dashed",
   "strokeStyle": "dashed",
   "strokeWidth": 2,
   "strokeWidth": 2,
   "type": "diamond",
   "type": "diamond",
@@ -160,10 +168,12 @@ Object {
   "points": Array [],
   "points": Array [],
   "pressures": Array [],
   "pressures": Array [],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "simulatePressure": true,
   "simulatePressure": true,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "freedraw",
   "type": "freedraw",
@@ -203,11 +213,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",
@@ -247,11 +259,13 @@ Object {
     ],
     ],
   ],
   ],
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "startArrowhead": null,
   "startArrowhead": null,
   "startBinding": null,
   "startBinding": null,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "type": "line",
   "type": "line",
@@ -283,9 +297,11 @@ Object {
   "opacity": 100,
   "opacity": 100,
   "originalText": "text",
   "originalText": "text",
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "text": "text",
   "text": "text",
@@ -320,9 +336,11 @@ Object {
   "opacity": 100,
   "opacity": 100,
   "originalText": "test",
   "originalText": "test",
   "roughness": 1,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": Any<Number>,
   "seed": Any<Number>,
   "strokeColor": "#000000",
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "strokeWidth": 1,
   "text": "",
   "text": "",

+ 2 - 2
src/tests/data/restore.test.ts

@@ -10,7 +10,7 @@ import { API } from "../helpers/api";
 import { getDefaultAppState } from "../../appState";
 import { getDefaultAppState } from "../../appState";
 import { ImportedDataState } from "../../data/types";
 import { ImportedDataState } from "../../data/types";
 import { NormalizedZoomValue } from "../../types";
 import { NormalizedZoomValue } from "../../types";
-import { FONT_FAMILY } from "../../constants";
+import { FONT_FAMILY, ROUNDNESS } from "../../constants";
 import { newElementWith } from "../../element/mutateElement";
 import { newElementWith } from "../../element/mutateElement";
 
 
 const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
 const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
@@ -255,7 +255,7 @@ describe("restoreElements", () => {
         width: 100,
         width: 100,
         height: 200,
         height: 200,
         groupIds: ["1", "2", "3"],
         groupIds: ["1", "2", "3"],
-        strokeSharpness: "round",
+        roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
       });
       });
 
 
       elements.push(element);
       elements.push(element);

+ 1 - 1
src/tests/fixtures/elementFixture.ts

@@ -15,7 +15,7 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
   roughness: 1,
   roughness: 1,
   opacity: 100,
   opacity: 100,
   groupIds: [],
   groupIds: [],
-  strokeSharpness: "sharp",
+  roundness: null,
   seed: 1041657908,
   seed: 1041657908,
   version: 120,
   version: 120,
   versionNonce: 1188004276,
   versionNonce: 1188004276,

+ 28 - 5
src/tests/helpers/api.ts

@@ -8,7 +8,7 @@ import {
   FileId,
   FileId,
 } from "../../element/types";
 } from "../../element/types";
 import { newElement, newTextElement, newLinearElement } from "../../element";
 import { newElement, newTextElement, newLinearElement } from "../../element";
-import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
+import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
 import { getDefaultAppState } from "../../appState";
 import { getDefaultAppState } from "../../appState";
 import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
 import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
 import fs from "fs";
 import fs from "fs";
@@ -18,6 +18,7 @@ import { getMimeType } from "../../data/blob";
 import { newFreeDrawElement, newImageElement } from "../../element/newElement";
 import { newFreeDrawElement, newImageElement } from "../../element/newElement";
 import { Point } from "../../types";
 import { Point } from "../../types";
 import { getSelectedElements } from "../../scene/selection";
 import { getSelectedElements } from "../../scene/selection";
+import { isLinearElementType } from "../../element/typeChecks";
 
 
 const readFile = util.promisify(fs.readFile);
 const readFile = util.promisify(fs.readFile);
 
 
@@ -89,7 +90,7 @@ export class API {
     fillStyle?: ExcalidrawGenericElement["fillStyle"];
     fillStyle?: ExcalidrawGenericElement["fillStyle"];
     strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
     strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
     strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
     strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
-    strokeSharpness?: ExcalidrawGenericElement["strokeSharpness"];
+    roundness?: ExcalidrawGenericElement["roundness"];
     roughness?: ExcalidrawGenericElement["roughness"];
     roughness?: ExcalidrawGenericElement["roughness"];
     opacity?: ExcalidrawGenericElement["opacity"];
     opacity?: ExcalidrawGenericElement["opacity"];
     // text props
     // text props
@@ -125,7 +126,20 @@ export class API {
 
 
     const appState = h?.state || getDefaultAppState();
     const appState = h?.state || getDefaultAppState();
 
 
-    const base = {
+    const base: Omit<
+      ExcalidrawGenericElement,
+      | "id"
+      | "width"
+      | "height"
+      | "type"
+      | "seed"
+      | "version"
+      | "versionNonce"
+      | "isDeleted"
+      | "groupIds"
+      | "link"
+      | "updated"
+    > = {
       x,
       x,
       y,
       y,
       angle: rest.angle ?? 0,
       angle: rest.angle ?? 0,
@@ -135,8 +149,17 @@ export class API {
       fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
       fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
       strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
       strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
       strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
       strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
-      strokeSharpness:
-        rest.strokeSharpness ?? appState.currentItemStrokeSharpness,
+      roundness: (
+        rest.roundness === undefined
+          ? appState.currentItemRoundness === "round"
+          : rest.roundness
+      )
+        ? {
+            type: isLinearElementType(type)
+              ? ROUNDNESS.PROPORTIONAL_RADIUS
+              : ROUNDNESS.ADAPTIVE_RADIUS,
+          }
+        : null,
       roughness: rest.roughness ?? appState.currentItemRoughness,
       roughness: rest.roughness ?? appState.currentItemRoughness,
       opacity: rest.opacity ?? appState.currentItemOpacity,
       opacity: rest.opacity ?? appState.currentItemOpacity,
       boundElements: rest.boundElements ?? null,
       boundElements: rest.boundElements ?? null,

+ 27 - 14
src/tests/linearElementEditor.test.tsx

@@ -20,6 +20,7 @@ import { resize, rotate } from "./utils";
 import { getBoundTextElementPosition, wrapText } from "../element/textElement";
 import { getBoundTextElementPosition, wrapText } from "../element/textElement";
 import { getMaxContainerWidth } from "../element/newElement";
 import { getMaxContainerWidth } from "../element/newElement";
 import * as textElementUtils from "../element/textElement";
 import * as textElementUtils from "../element/textElement";
+import { ROUNDNESS } from "../constants";
 
 
 const renderScene = jest.spyOn(Renderer, "renderScene");
 const renderScene = jest.spyOn(Renderer, "renderScene");
 
 
@@ -51,7 +52,7 @@ describe("Test Linear Elements", () => {
 
 
   const createTwoPointerLinearElement = (
   const createTwoPointerLinearElement = (
     type: ExcalidrawLinearElement["type"],
     type: ExcalidrawLinearElement["type"],
-    strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
+    roundness: ExcalidrawElement["roundness"] = null,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
   ) => {
   ) => {
     const line = API.createElement({
     const line = API.createElement({
@@ -65,7 +66,7 @@ describe("Test Linear Elements", () => {
         [0, 0],
         [0, 0],
         [p2[0] - p1[0], p2[1] - p1[1]],
         [p2[0] - p1[0], p2[1] - p1[1]],
       ],
       ],
-      strokeSharpness,
+      roundness,
     });
     });
     h.elements = [line];
     h.elements = [line];
 
 
@@ -75,7 +76,7 @@ describe("Test Linear Elements", () => {
 
 
   const createThreePointerLinearElement = (
   const createThreePointerLinearElement = (
     type: ExcalidrawLinearElement["type"],
     type: ExcalidrawLinearElement["type"],
-    strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
+    roundness: ExcalidrawElement["roundness"] = null,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
   ) => {
   ) => {
     //dragging line from midpoint
     //dragging line from midpoint
@@ -92,7 +93,7 @@ describe("Test Linear Elements", () => {
         [p3[0], p3[1]],
         [p3[0], p3[1]],
         [p2[0] - p1[0], p2[1] - p1[1]],
         [p2[0] - p1[0], p2[1] - p1[1]],
       ],
       ],
-      strokeSharpness,
+      roundness,
     });
     });
     h.elements = [line];
     h.elements = [line];
     mouse.clickAt(p1[0], p1[1]);
     mouse.clickAt(p1[0], p1[1]);
@@ -286,7 +287,7 @@ describe("Test Linear Elements", () => {
       `);
       `);
     });
     });
 
 
-    it("should update the midpoints when element sharpness changed", async () => {
+    it("should update the midpoints when element roundness changed", async () => {
       createThreePointerLinearElement("line");
       createThreePointerLinearElement("line");
 
 
       const line = h.elements[0] as ExcalidrawLinearElement;
       const line = h.elements[0] as ExcalidrawLinearElement;
@@ -299,7 +300,7 @@ describe("Test Linear Elements", () => {
         h.state,
         h.state,
       );
       );
 
 
-      // update sharpness
+      // update roundness
       fireEvent.click(screen.getByTitle("Round"));
       fireEvent.click(screen.getByTitle("Round"));
 
 
       expect(renderScene).toHaveBeenCalledTimes(12);
       expect(renderScene).toHaveBeenCalledTimes(12);
@@ -325,7 +326,9 @@ describe("Test Linear Elements", () => {
     });
     });
 
 
     it("should update all the midpoints when element position changed", async () => {
     it("should update all the midpoints when element position changed", async () => {
-      createThreePointerLinearElement("line", "round");
+      createThreePointerLinearElement("line", {
+        type: ROUNDNESS.PROPORTIONAL_RADIUS,
+      });
 
 
       const line = h.elements[0] as ExcalidrawLinearElement;
       const line = h.elements[0] as ExcalidrawLinearElement;
       expect(line.points.length).toEqual(3);
       expect(line.points.length).toEqual(3);
@@ -370,8 +373,8 @@ describe("Test Linear Elements", () => {
       `);
       `);
     });
     });
 
 
-    describe("When edges are sharp", () => {
-      // This is the expected midpoint for line with sharp edge
+    describe("When edges are round", () => {
+      // This is the expected midpoint for line with round edge
       // hence hardcoding it so if later some bug is introduced
       // hence hardcoding it so if later some bug is introduced
       // this will fail and we can fix it
       // this will fail and we can fix it
       const firstSegmentMidpoint: Point = [55, 45];
       const firstSegmentMidpoint: Point = [55, 45];
@@ -525,7 +528,9 @@ describe("Test Linear Elements", () => {
       let line: ExcalidrawLinearElement;
       let line: ExcalidrawLinearElement;
 
 
       beforeEach(() => {
       beforeEach(() => {
-        line = createThreePointerLinearElement("line", "round");
+        line = createThreePointerLinearElement("line", {
+          type: ROUNDNESS.PROPORTIONAL_RADIUS,
+        });
         expect(line.points.length).toEqual(3);
         expect(line.points.length).toEqual(3);
 
 
         enterLineEditingMode(line);
         enterLineEditingMode(line);
@@ -768,7 +773,9 @@ describe("Test Linear Elements", () => {
       });
       });
 
 
       it("should return correct position for arrow with odd points", () => {
       it("should return correct position for arrow with odd points", () => {
-        createThreePointerLinearElement("arrow", "round");
+        createThreePointerLinearElement("arrow", {
+          type: ROUNDNESS.PROPORTIONAL_RADIUS,
+        });
         const arrow = h.elements[0] as ExcalidrawLinearElement;
         const arrow = h.elements[0] as ExcalidrawLinearElement;
         const { textElement, container } = createBoundTextElement(
         const { textElement, container } = createBoundTextElement(
           DEFAULT_TEXT,
           DEFAULT_TEXT,
@@ -788,7 +795,9 @@ describe("Test Linear Elements", () => {
       });
       });
 
 
       it("should return correct position for arrow with even points", () => {
       it("should return correct position for arrow with even points", () => {
-        createThreePointerLinearElement("arrow", "round");
+        createThreePointerLinearElement("arrow", {
+          type: ROUNDNESS.PROPORTIONAL_RADIUS,
+        });
         const arrow = h.elements[0] as ExcalidrawLinearElement;
         const arrow = h.elements[0] as ExcalidrawLinearElement;
         const { textElement, container } = createBoundTextElement(
         const { textElement, container } = createBoundTextElement(
           DEFAULT_TEXT,
           DEFAULT_TEXT,
@@ -903,7 +912,9 @@ describe("Test Linear Elements", () => {
     });
     });
 
 
     it("should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated", () => {
     it("should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated", () => {
-      createThreePointerLinearElement("arrow", "round");
+      createThreePointerLinearElement("arrow", {
+        type: ROUNDNESS.PROPORTIONAL_RADIUS,
+      });
 
 
       const arrow = h.elements[0] as ExcalidrawLinearElement;
       const arrow = h.elements[0] as ExcalidrawLinearElement;
 
 
@@ -967,7 +978,9 @@ describe("Test Linear Elements", () => {
     });
     });
 
 
     it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
     it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
-      createThreePointerLinearElement("arrow", "round");
+      createThreePointerLinearElement("arrow", {
+        type: ROUNDNESS.PROPORTIONAL_RADIUS,
+      });
 
 
       const arrow = h.elements[0] as ExcalidrawLinearElement;
       const arrow = h.elements[0] as ExcalidrawLinearElement;
 
 

+ 1 - 2
src/tests/packages/__snapshots__/utils.test.ts.snap

@@ -15,12 +15,11 @@ Object {
   "currentItemFillStyle": "hachure",
   "currentItemFillStyle": "hachure",
   "currentItemFontFamily": 1,
   "currentItemFontFamily": 1,
   "currentItemFontSize": 20,
   "currentItemFontSize": 20,
-  "currentItemLinearStrokeSharpness": "round",
   "currentItemOpacity": 100,
   "currentItemOpacity": 100,
   "currentItemRoughness": 1,
   "currentItemRoughness": 1,
+  "currentItemRoundness": "round",
   "currentItemStartArrowhead": null,
   "currentItemStartArrowhead": null,
   "currentItemStrokeColor": "#000000",
   "currentItemStrokeColor": "#000000",
-  "currentItemStrokeSharpness": "sharp",
   "currentItemStrokeStyle": "solid",
   "currentItemStrokeStyle": "solid",
   "currentItemStrokeWidth": 1,
   "currentItemStrokeWidth": 1,
   "currentItemTextAlign": "left",
   "currentItemTextAlign": "left",

+ 1 - 1
src/tests/scene/__snapshots__/export.test.ts.snap

@@ -95,7 +95,7 @@ exports[`exportToSvg with elements that have a link 1`] = `
 exports[`exportToSvg with exportEmbedScene 1`] = `
 exports[`exportToSvg with exportEmbedScene 1`] = `
 "
 "
   <!-- svg-source:excalidraw -->
   <!-- svg-source:excalidraw -->
-  <!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SPW/CMFx1MDAxMN35XHUwMDE1kbtcIpGk4aNstFRVpapcdTAwMWRcdTAwMTiQWnUw8YVYMbaxXHUwMDFkPoT477VccsRtxNpcclx1MDAwZpbu+b278907dKJcYpm9XHUwMDA0NI5cdTAwMTDscswoUXiLulx1MDAwZd+A0lRw+5T6WIta5Z5ZXHUwMDFhI8e9XHUwMDFlXHUwMDEzVlBcbm1OfGCwXHUwMDAybrRlfNk4ilx1MDAwZf62L5Q41Wau1lx1MDAxZpOiopyk63w1fJtOXj691JN2lpMlWVx1MDAxM+9d4fthXHUwMDEzbykxpcWSOG6wXHUwMDEy6LI0LVx1MDAxMPMlc21cdTAwMDZEXHUwMDFiJSp4XHUwMDEyTCjXyF3sTyi9wHm1VKLmJHCSPsaLXCJwXG7K2Mzs2WlcdTAwMDA4L2tcdTAwMDWoVWF+abGFNzot7ICDypZcXJZcdTAwMWO0/qNcdTAwMTFcdTAwMTLn1Oxbv3L9yVfip/vdzl9iJc95kHbBr85cdTAwMDCIT5Ulg/7wIVx1MDAxZTUvYb9JXHUwMDFht9F3wf2uk2Q0iuMsXHUwMDFkXHUwMDBlXHUwMDFhXHUwMDA21VO7auPTXHUwMDE2mGlcYnN0I3xcdTAwMGU24DVjzWMtXHQ+icJXXHUwMDE55VWbZ11VXcl9cSmheCU4QVx1MDAxZT92b0a7XHUwMDE57X+MXHUwMDA2jFGp4Ww0e/thICzlzNj8lnKyXHUwMDFk2lDYPl5ZbOGP03ubusWCa/Zw7Fx1MDAxY39cdTAwMDCLqmbvIn0=<!-- payload-end -->
+  <!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SsW7CMFx1MDAxMN35iihdkUjSQChcdTAwMWItVVWpalx1MDAwN1x1MDAwNqRWXHUwMDFkTHxJrFx1MDAxONvEXHUwMDBlXHUwMDEwIf69tlx1MDAwM3GJXHUwMDE4O9aDpXt+7+58945cdTAwMDPP81UjwJ95Plx1MDAxY1JEXHSu0N5cdTAwMWZcdTAwMWF8XHUwMDA3lSSc6afIxpLXVWqZhVJiNlx1MDAxYVGuXHUwMDA1XHUwMDA1l6rlXHUwMDAzhVxyMCU140vHnne0t34h2Kh2q2r7Mc9KwnC0TTfJ22L+8mmllnTQnDiMu7gxhe+TLt5cdTAwMTOsXG6NhUHQYVx1MDAwNZC8UD1cdTAwMTCxnJo2XHUwMDFkXCJVxUt44pRXppG7wFx1MDAxZVd6jdIyr3jNsOOEY4TWmeNkhNKlamg7XHUwMDAwlFx1MDAxNnVcdTAwMDV+r8Lq0mJcdTAwMGbvdJLrXHUwMDAxO5UumVx1MDAxN1xmpLzScIFSoprer0x/4lx1MDAxNdvpfv/OwPA5XHUwMDAzqyl1hVx1MDAwMbDNXHUwMDEwh5Nx8lx1MDAxMEy7XHUwMDE3t9YwXG766DtndsVhOJ1cdTAwMDZBXHUwMDFjJZOOQeRCb1jZtFx1MDAxOaJcdTAwMTLc+ExcdTAwMTPPbvtXjdRcdTAwMDKjVuR+SFx0K/s8babyRu6LOTFBXHUwMDFizrBv8dPw31///vpTf1x1MDAwMaVESDj7S992XHUwMDA2Plx1MDAxMmKpdH5Nad3m71xi7Fx1MDAxZm/sM7PH6K07zT7BNHs8XHJOP7VXYMUifQ==<!-- payload-end -->
   <defs>
   <defs>
     <style class=\\"style-fonts\\">
     <style class=\\"style-fonts\\">
       @font-face {
       @font-face {

+ 2 - 2
src/types.ts

@@ -13,6 +13,7 @@ import {
   FileId,
   FileId,
   ExcalidrawImageElement,
   ExcalidrawImageElement,
   Theme,
   Theme,
+  StrokeRoundness,
 } from "./element/types";
 } from "./element/types";
 import { SHAPES } from "./shapes";
 import { SHAPES } from "./shapes";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -134,10 +135,9 @@ export type AppState = {
   currentItemFontFamily: FontFamilyValues;
   currentItemFontFamily: FontFamilyValues;
   currentItemFontSize: number;
   currentItemFontSize: number;
   currentItemTextAlign: TextAlign;
   currentItemTextAlign: TextAlign;
-  currentItemStrokeSharpness: ExcalidrawElement["strokeSharpness"];
   currentItemStartArrowhead: Arrowhead | null;
   currentItemStartArrowhead: Arrowhead | null;
   currentItemEndArrowhead: Arrowhead | null;
   currentItemEndArrowhead: Arrowhead | null;
-  currentItemLinearStrokeSharpness: ExcalidrawElement["strokeSharpness"];
+  currentItemRoundness: StrokeRoundness;
   viewBackgroundColor: string;
   viewBackgroundColor: string;
   scrollX: number;
   scrollX: number;
   scrollY: number;
   scrollY: number;

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است