Переглянути джерело

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;
   if (isLinearElement(element)) {
-    initialPointsCoords = getElementPointsCoords(
-      element,
-      element.points,
-      element.strokeSharpness,
-    );
+    initialPointsCoords = getElementPointsCoords(element, element.points);
   }
   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.
     // 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.
-    const finalPointsCoords = getElementPointsCoords(
-      element,
-      element.points,
-      element.strokeSharpness,
-    );
+    const finalPointsCoords = getElementPointsCoords(element, element.points);
 
     const topLeftCoordsDiff = initialPointsCoords[0] - finalPointsCoords[0];
     const topRightCoordDiff = initialPointsCoords[2] - finalPointsCoords[2];

+ 55 - 52
src/actions/actionProperties.tsx

@@ -42,6 +42,7 @@ import {
   DEFAULT_FONT_FAMILY,
   DEFAULT_FONT_SIZE,
   FONT_FAMILY,
+  ROUNDNESS,
   VERTICAL_ALIGN,
 } from "../constants";
 import {
@@ -57,7 +58,7 @@ import {
 import {
   isBoundToContainer,
   isLinearElement,
-  isLinearElementType,
+  isUsingAdaptiveRadius,
 } from "../element/typeChecks";
 import {
   Arrowhead,
@@ -72,7 +73,7 @@ import { getLanguage, t } from "../i18n";
 import { KEYS } from "../keys";
 import { randomInteger } from "../random";
 import {
-  canChangeSharpness,
+  canChangeRoundness,
   canHaveArrowheads,
   getCommonAttributeOfSelectedElements,
   getSelectedElements,
@@ -848,69 +849,71 @@ export const actionChangeVerticalAlign = register({
   },
 });
 
-export const actionChangeSharpness = register({
-  name: "changeSharpness",
+export const actionChangeRoundness = register({
+  name: "changeRoundness",
   trackEvent: false,
   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 {
       elements: changeProperty(elements, appState, (el) =>
         newElementWith(el, {
-          strokeSharpness: value,
+          roundness:
+            value === "round"
+              ? {
+                  type: isUsingAdaptiveRadius(el.type)
+                    ? ROUNDNESS.ADAPTIVE_RADIUS
+                    : ROUNDNESS.PROPORTIONAL_RADIUS,
+                }
+              : null,
         }),
       ),
       appState: {
         ...appState,
-        currentItemStrokeSharpness: shouldUpdateForNonLinearElements
-          ? value
-          : appState.currentItemStrokeSharpness,
-        currentItemLinearStrokeSharpness: shouldUpdateForLinearElements
-          ? value
-          : appState.currentItemLinearStrokeSharpness,
+        currentItemRoundness: value,
       },
       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({

+ 1 - 0
src/actions/actionStyles.ts

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

+ 1 - 1
src/actions/types.ts

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

+ 2 - 4
src/appState.ts

@@ -28,12 +28,11 @@ export const getDefaultAppState = (): Omit<
     currentItemFillStyle: "hachure",
     currentItemFontFamily: DEFAULT_FONT_FAMILY,
     currentItemFontSize: DEFAULT_FONT_SIZE,
-    currentItemLinearStrokeSharpness: "round",
     currentItemOpacity: 100,
     currentItemRoughness: 1,
     currentItemStartArrowhead: null,
     currentItemStrokeColor: oc.black,
-    currentItemStrokeSharpness: "sharp",
+    currentItemRoundness: "round",
     currentItemStrokeStyle: "solid",
     currentItemStrokeWidth: 1,
     currentItemTextAlign: DEFAULT_TEXT_ALIGN,
@@ -120,7 +119,7 @@ const APP_STATE_STORAGE_CONF = (<
   currentItemFillStyle: { browser: true, export: false, server: false },
   currentItemFontFamily: { browser: true, export: false, server: false },
   currentItemFontSize: { browser: true, export: false, server: false },
-  currentItemLinearStrokeSharpness: {
+  currentItemRoundness: {
     browser: true,
     export: false,
     server: false,
@@ -129,7 +128,6 @@ const APP_STATE_STORAGE_CONF = (<
   currentItemRoughness: { browser: true, export: false, server: false },
   currentItemStartArrowhead: { 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 },
   currentItemStrokeWidth: { 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,
   roughness: 1,
   strokeColor: colors.elementStroke[0],
-  strokeSharpness: "sharp",
+  roundness: null,
   strokeStyle: "solid",
   strokeWidth: 1,
   verticalAlign: VERTICAL_ALIGN.MIDDLE,
@@ -322,7 +322,7 @@ const chartBaseElements = (
         text: spreadsheet.title,
         x: x + chartWidth / 2,
         y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
-        strokeSharpness: "sharp",
+        roundness: null,
         strokeStyle: "solid",
         textAlign: "center",
       })

+ 4 - 4
src/components/Actions.tsx

@@ -5,7 +5,7 @@ import { ExcalidrawElement, PointerType } from "../element/types";
 import { t } from "../i18n";
 import { useDevice } from "../components/App";
 import {
-  canChangeSharpness,
+  canChangeRoundness,
   canHaveArrowheads,
   getTargetElements,
   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) ||

+ 18 - 6
src/components/App.tsx

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

+ 26 - 0
src/constants.ts

@@ -216,6 +216,32 @@ export const TEXT_ALIGN = {
 
 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 = {
   AUTH_STATE_COOKIE: "excplus-auth",
 } as const;

+ 16 - 4
src/data/restore.ts

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

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

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

+ 8 - 13
src/element/bounds.ts

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

+ 9 - 3
src/element/collision.ts

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

+ 4 - 12
src/element/linearElementEditor.ts

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

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

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

+ 4 - 3
src/element/newElement.ts

@@ -62,14 +62,15 @@ const _newElementBase = <T extends ExcalidrawElement>(
     height = 0,
     angle = 0,
     groupIds = [],
-    strokeSharpness,
+    roundness = null,
     boundElements = null,
     link = null,
     locked,
     ...rest
   }: 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(),
     type,
     x,
@@ -85,7 +86,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
     roughness,
     opacity,
     groupIds,
-    strokeSharpness,
+    roundness,
     seed: rest.seed ?? randomInteger(),
     version: rest.version || 1,
     versionNonce: rest.versionNonce ?? 0,

+ 2 - 0
src/element/typeChecks.ts

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

+ 10 - 3
src/element/types.ts

@@ -1,5 +1,11 @@
 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 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 GroupId = string;
 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 TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
 
@@ -25,7 +32,7 @@ type _ExcalidrawElementBase = Readonly<{
   fillStyle: FillStyle;
   strokeWidth: number;
   strokeStyle: StrokeStyle;
-  strokeSharpness: StrokeSharpness;
+  roundness: null | { type: RoundnessType; value?: number };
   roughness: number;
   opacity: number;
   width: number;

+ 34 - 2
src/math.ts

@@ -1,6 +1,15 @@
 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 { getCurvePathOps } from "./element/bounds";
 
@@ -266,6 +275,29 @@ export const getGridPoint = (
   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 = (
   element: NonDeleted<ExcalidrawLinearElement>,
   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.
 -->
 
+## 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)
 
 ### Excalidraw API

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

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

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

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

+ 30 - 26
src/renderer/renderElement.ts

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

+ 1 - 1
src/scene/comparisons.ts

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

+ 1 - 1
src/scene/index.ts

@@ -12,7 +12,7 @@ export {
   hasStrokeWidth,
   hasStrokeStyle,
   canHaveArrowheads,
-  canChangeSharpness,
+  canChangeRoundness,
   getElementAtPosition,
   hasText,
   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,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "startArrowhead": null,
   "startBinding": null,
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
@@ -62,9 +64,11 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "diamond",
@@ -93,9 +97,11 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "ellipse",
@@ -135,11 +141,13 @@ Object {
     ],
   ],
   "roughness": 1,
+  "roundness": Object {
+    "type": 2,
+  },
   "seed": 337897,
   "startArrowhead": null,
   "startBinding": null,
   "strokeColor": "#000000",
-  "strokeSharpness": "round",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
@@ -168,9 +176,11 @@ Object {
   "locked": false,
   "opacity": 100,
   "roughness": 1,
+  "roundness": Object {
+    "type": 3,
+  },
   "seed": 337897,
   "strokeColor": "#000000",
-  "strokeSharpness": "sharp",
   "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",

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

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

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

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

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

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

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

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

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

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

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

@@ -8,7 +8,7 @@ import {
   FileId,
 } from "../../element/types";
 import { newElement, newTextElement, newLinearElement } from "../../element";
-import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
+import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
 import { getDefaultAppState } from "../../appState";
 import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
 import fs from "fs";
@@ -18,6 +18,7 @@ import { getMimeType } from "../../data/blob";
 import { newFreeDrawElement, newImageElement } from "../../element/newElement";
 import { Point } from "../../types";
 import { getSelectedElements } from "../../scene/selection";
+import { isLinearElementType } from "../../element/typeChecks";
 
 const readFile = util.promisify(fs.readFile);
 
@@ -89,7 +90,7 @@ export class API {
     fillStyle?: ExcalidrawGenericElement["fillStyle"];
     strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
     strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
-    strokeSharpness?: ExcalidrawGenericElement["strokeSharpness"];
+    roundness?: ExcalidrawGenericElement["roundness"];
     roughness?: ExcalidrawGenericElement["roughness"];
     opacity?: ExcalidrawGenericElement["opacity"];
     // text props
@@ -125,7 +126,20 @@ export class API {
 
     const appState = h?.state || getDefaultAppState();
 
-    const base = {
+    const base: Omit<
+      ExcalidrawGenericElement,
+      | "id"
+      | "width"
+      | "height"
+      | "type"
+      | "seed"
+      | "version"
+      | "versionNonce"
+      | "isDeleted"
+      | "groupIds"
+      | "link"
+      | "updated"
+    > = {
       x,
       y,
       angle: rest.angle ?? 0,
@@ -135,8 +149,17 @@ export class API {
       fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
       strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
       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,
       opacity: rest.opacity ?? appState.currentItemOpacity,
       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 { getMaxContainerWidth } from "../element/newElement";
 import * as textElementUtils from "../element/textElement";
+import { ROUNDNESS } from "../constants";
 
 const renderScene = jest.spyOn(Renderer, "renderScene");
 
@@ -51,7 +52,7 @@ describe("Test Linear Elements", () => {
 
   const createTwoPointerLinearElement = (
     type: ExcalidrawLinearElement["type"],
-    strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
+    roundness: ExcalidrawElement["roundness"] = null,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
   ) => {
     const line = API.createElement({
@@ -65,7 +66,7 @@ describe("Test Linear Elements", () => {
         [0, 0],
         [p2[0] - p1[0], p2[1] - p1[1]],
       ],
-      strokeSharpness,
+      roundness,
     });
     h.elements = [line];
 
@@ -75,7 +76,7 @@ describe("Test Linear Elements", () => {
 
   const createThreePointerLinearElement = (
     type: ExcalidrawLinearElement["type"],
-    strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
+    roundness: ExcalidrawElement["roundness"] = null,
     roughness: ExcalidrawLinearElement["roughness"] = 0,
   ) => {
     //dragging line from midpoint
@@ -92,7 +93,7 @@ describe("Test Linear Elements", () => {
         [p3[0], p3[1]],
         [p2[0] - p1[0], p2[1] - p1[1]],
       ],
-      strokeSharpness,
+      roundness,
     });
     h.elements = [line];
     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");
 
       const line = h.elements[0] as ExcalidrawLinearElement;
@@ -299,7 +300,7 @@ describe("Test Linear Elements", () => {
         h.state,
       );
 
-      // update sharpness
+      // update roundness
       fireEvent.click(screen.getByTitle("Round"));
 
       expect(renderScene).toHaveBeenCalledTimes(12);
@@ -325,7 +326,9 @@ describe("Test Linear Elements", () => {
     });
 
     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;
       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
       // this will fail and we can fix it
       const firstSegmentMidpoint: Point = [55, 45];
@@ -525,7 +528,9 @@ describe("Test Linear Elements", () => {
       let line: ExcalidrawLinearElement;
 
       beforeEach(() => {
-        line = createThreePointerLinearElement("line", "round");
+        line = createThreePointerLinearElement("line", {
+          type: ROUNDNESS.PROPORTIONAL_RADIUS,
+        });
         expect(line.points.length).toEqual(3);
 
         enterLineEditingMode(line);
@@ -768,7 +773,9 @@ describe("Test Linear Elements", () => {
       });
 
       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 { textElement, container } = createBoundTextElement(
           DEFAULT_TEXT,
@@ -788,7 +795,9 @@ describe("Test Linear Elements", () => {
       });
 
       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 { textElement, container } = createBoundTextElement(
           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", () => {
-      createThreePointerLinearElement("arrow", "round");
+      createThreePointerLinearElement("arrow", {
+        type: ROUNDNESS.PROPORTIONAL_RADIUS,
+      });
 
       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", () => {
-      createThreePointerLinearElement("arrow", "round");
+      createThreePointerLinearElement("arrow", {
+        type: ROUNDNESS.PROPORTIONAL_RADIUS,
+      });
 
       const arrow = h.elements[0] as ExcalidrawLinearElement;
 

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

@@ -15,12 +15,11 @@ Object {
   "currentItemFillStyle": "hachure",
   "currentItemFontFamily": 1,
   "currentItemFontSize": 20,
-  "currentItemLinearStrokeSharpness": "round",
   "currentItemOpacity": 100,
   "currentItemRoughness": 1,
+  "currentItemRoundness": "round",
   "currentItemStartArrowhead": null,
   "currentItemStrokeColor": "#000000",
-  "currentItemStrokeSharpness": "sharp",
   "currentItemStrokeStyle": "solid",
   "currentItemStrokeWidth": 1,
   "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`] = `
 "
   <!-- 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>
     <style class=\\"style-fonts\\">
       @font-face {

+ 2 - 2
src/types.ts

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

Деякі файли не було показано, через те що забагато файлів було змінено