Jelajahi Sumber

implement stroke style (#1571)

David Luzar 5 tahun lalu
induk
melakukan
39c56a4c01

+ 35 - 0
src/actions/actionProperties.tsx

@@ -227,6 +227,41 @@ export const actionChangeSloppiness = register({
   ),
 });
 
+export const actionChangeStrokeStyle = register({
+  name: "changeStrokeStyle",
+  perform: (elements, appState, value) => {
+    return {
+      elements: changeProperty(elements, appState, (el) =>
+        newElementWith(el, {
+          strokeStyle: value,
+        }),
+      ),
+      appState: { ...appState, currentItemStrokeStyle: value },
+      commitToHistory: true,
+    };
+  },
+  PanelComponent: ({ elements, appState, updateData }) => (
+    <fieldset>
+      <legend>{t("labels.strokeStyle")}</legend>
+      <ButtonSelect
+        group="strokeStyle"
+        options={[
+          { value: "solid", text: t("labels.strokeStyle_solid") },
+          { value: "dashed", text: t("labels.strokeStyle_dashed") },
+          { value: "dotted", text: t("labels.strokeStyle_dotted") },
+        ]}
+        value={getFormValue(
+          elements,
+          appState,
+          (element) => element.strokeStyle,
+          appState.currentItemStrokeStyle,
+        )}
+        onChange={(value) => updateData(value)}
+      />
+    </fieldset>
+  ),
+});
+
 export const actionChangeOpacity = register({
   name: "changeOpacity",
   perform: (elements, appState, value) => {

+ 1 - 0
src/actions/types.ts

@@ -30,6 +30,7 @@ export type ActionName =
   | "changeFillStyle"
   | "changeStrokeWidth"
   | "changeSloppiness"
+  | "changeStrokeStyle"
   | "changeOpacity"
   | "changeFontSize"
   | "toggleCanvasMenu"

+ 1 - 0
src/appState.ts

@@ -22,6 +22,7 @@ export function getDefaultAppState(): AppState {
     currentItemBackgroundColor: "transparent",
     currentItemFillStyle: "hachure",
     currentItemStrokeWidth: 1,
+    currentItemStrokeStyle: "solid",
     currentItemRoughness: 1,
     currentItemOpacity: 100,
     currentItemFont: DEFAULT_FONT,

+ 1 - 1
src/components/Actions.tsx

@@ -45,7 +45,7 @@ export function SelectedShapeActions({
         targetElements.some((element) => hasStroke(element.type))) && (
         <>
           {renderAction("changeStrokeWidth")}
-
+          {renderAction("changeStrokeStyle")}
           {renderAction("changeSloppiness")}
         </>
       )}

+ 4 - 0
src/components/App.tsx

@@ -729,6 +729,7 @@ class App extends React.Component<any, AppState> {
       backgroundColor: this.state.currentItemBackgroundColor,
       fillStyle: this.state.currentItemFillStyle,
       strokeWidth: this.state.currentItemStrokeWidth,
+      strokeStyle: this.state.currentItemStrokeStyle,
       roughness: this.state.currentItemRoughness,
       opacity: this.state.currentItemOpacity,
       text: text,
@@ -1357,6 +1358,7 @@ class App extends React.Component<any, AppState> {
             backgroundColor: this.state.currentItemBackgroundColor,
             fillStyle: this.state.currentItemFillStyle,
             strokeWidth: this.state.currentItemStrokeWidth,
+            strokeStyle: this.state.currentItemStrokeStyle,
             roughness: this.state.currentItemRoughness,
             opacity: this.state.currentItemOpacity,
             text: "",
@@ -2037,6 +2039,7 @@ class App extends React.Component<any, AppState> {
           backgroundColor: this.state.currentItemBackgroundColor,
           fillStyle: this.state.currentItemFillStyle,
           strokeWidth: this.state.currentItemStrokeWidth,
+          strokeStyle: this.state.currentItemStrokeStyle,
           roughness: this.state.currentItemRoughness,
           opacity: this.state.currentItemOpacity,
         });
@@ -2067,6 +2070,7 @@ class App extends React.Component<any, AppState> {
         backgroundColor: this.state.currentItemBackgroundColor,
         fillStyle: this.state.currentItemFillStyle,
         strokeWidth: this.state.currentItemStrokeWidth,
+        strokeStyle: this.state.currentItemStrokeStyle,
         roughness: this.state.currentItemRoughness,
         opacity: this.state.currentItemOpacity,
       });

+ 1 - 0
src/data/restore.ts

@@ -76,6 +76,7 @@ export function restore(
         id: element.id || randomId(),
         fillStyle: element.fillStyle || "hachure",
         strokeWidth: element.strokeWidth || 1,
+        strokeStyle: element.strokeStyle ?? "solid",
         roughness: element.roughness ?? 1,
         opacity:
           element.opacity === null || element.opacity === undefined

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

@@ -30,6 +30,7 @@ it("clones arrow element", () => {
     backgroundColor: "transparent",
     fillStyle: "hachure",
     strokeWidth: 1,
+    strokeStyle: "solid",
     roughness: 1,
     opacity: 100,
   });
@@ -73,6 +74,7 @@ it("clones text element", () => {
     backgroundColor: "transparent",
     fillStyle: "hachure",
     strokeWidth: 1,
+    strokeStyle: "solid",
     roughness: 1,
     opacity: 100,
     text: "hello",

+ 3 - 0
src/element/newElement.ts

@@ -17,6 +17,7 @@ type ElementConstructorOpts = {
   backgroundColor: ExcalidrawGenericElement["backgroundColor"];
   fillStyle: ExcalidrawGenericElement["fillStyle"];
   strokeWidth: ExcalidrawGenericElement["strokeWidth"];
+  strokeStyle: ExcalidrawGenericElement["strokeStyle"];
   roughness: ExcalidrawGenericElement["roughness"];
   opacity: ExcalidrawGenericElement["opacity"];
   width?: ExcalidrawGenericElement["width"];
@@ -33,6 +34,7 @@ function _newElementBase<T extends ExcalidrawElement>(
     backgroundColor,
     fillStyle,
     strokeWidth,
+    strokeStyle,
     roughness,
     opacity,
     width = 0,
@@ -53,6 +55,7 @@ function _newElementBase<T extends ExcalidrawElement>(
     backgroundColor,
     fillStyle,
     strokeWidth,
+    strokeStyle,
     roughness,
     opacity,
     seed: rest.seed ?? randomInteger(),

+ 1 - 0
src/element/types.ts

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

+ 4 - 0
src/locales/en.json

@@ -16,6 +16,10 @@
     "background": "Background",
     "fill": "Fill",
     "strokeWidth": "Stroke width",
+    "strokeStyle": "Stroke style",
+    "strokeStyle_solid": "Solid",
+    "strokeStyle_dashed": "Dashed",
+    "strokeStyle_dotted": "Dotted",
     "sloppiness": "Sloppiness",
     "opacity": "Opacity",
     "textAlign": "Text align",

+ 56 - 8
src/renderer/renderElement.ts

@@ -20,6 +20,9 @@ import rough from "roughjs/bin/rough";
 
 const CANVAS_PADDING = 20;
 
+const DASHARRAY_DASHED = [12, 8];
+const DASHARRAY_DOTTED = [3, 6];
+
 export interface ExcalidrawElementWithCanvas {
   element: ExcalidrawElement | ExcalidrawTextElement;
   canvas: HTMLCanvasElement;
@@ -90,9 +93,9 @@ function drawElementOnCanvas(
     case "arrow":
     case "draw":
     case "line": {
-      (getShapeForElement(element) as Drawable[]).forEach((shape) =>
-        rc.draw(shape),
-      );
+      (getShapeForElement(element) as Drawable[]).forEach((shape) => {
+        rc.draw(shape);
+      });
       break;
     }
     default: {
@@ -157,16 +160,42 @@ function generateElement(
   let shape = shapeCache.get(element) || null;
   if (!shape) {
     elementWithCanvasCache.delete(element);
+
+    const strokeLineDash =
+      element.strokeStyle === "dashed"
+        ? DASHARRAY_DASHED
+        : element.strokeStyle === "dotted"
+        ? DASHARRAY_DOTTED
+        : undefined;
+    // for non-solid strokes, disable multiStroke because it tends to make
+    //  dashes/dots overlay each other
+    const disableMultiStroke = element.strokeStyle !== "solid";
+    // for non-solid strokes, increase the width a bit to make it visually
+    //  similar to solid strokes, because we're also disabling multiStroke
+    const strokeWidth =
+      element.strokeStyle !== "solid"
+        ? element.strokeWidth + 0.5
+        : element.strokeWidth;
+    // when increasing strokeWidth, we must explicitly set fillWeight and
+    //  hachureGap because if not specified, roughjs uses strokeWidth to
+    //  calculate them (and we don't want the fills to be modified)
+    const fillWeight = element.strokeWidth / 2;
+    const hachureGap = element.strokeWidth * 4;
+
     switch (element.type) {
       case "rectangle":
         shape = generator.rectangle(0, 0, element.width, element.height, {
+          strokeWidth,
+          fillWeight,
+          hachureGap,
+          strokeLineDash,
+          disableMultiStroke,
           stroke: element.strokeColor,
           fill:
             element.backgroundColor === "transparent"
               ? undefined
               : element.backgroundColor,
           fillStyle: element.fillStyle,
-          strokeWidth: element.strokeWidth,
           roughness: element.roughness,
           seed: element.seed,
         });
@@ -191,13 +220,17 @@ function generateElement(
             [leftX, leftY],
           ],
           {
+            strokeWidth,
+            fillWeight,
+            hachureGap,
+            strokeLineDash,
+            disableMultiStroke,
             stroke: element.strokeColor,
             fill:
               element.backgroundColor === "transparent"
                 ? undefined
                 : element.backgroundColor,
             fillStyle: element.fillStyle,
-            strokeWidth: element.strokeWidth,
             roughness: element.roughness,
             seed: element.seed,
           },
@@ -211,13 +244,17 @@ function generateElement(
           element.width,
           element.height,
           {
+            strokeWidth,
+            fillWeight,
+            hachureGap,
+            strokeLineDash,
+            disableMultiStroke,
             stroke: element.strokeColor,
             fill:
               element.backgroundColor === "transparent"
                 ? undefined
                 : element.backgroundColor,
             fillStyle: element.fillStyle,
-            strokeWidth: element.strokeWidth,
             roughness: element.roughness,
             seed: element.seed,
             curveFitting: 1,
@@ -228,10 +265,14 @@ function generateElement(
       case "draw":
       case "arrow": {
         const options: Options = {
+          strokeWidth,
+          fillWeight,
+          hachureGap,
+          strokeLineDash,
+          disableMultiStroke,
           stroke: element.strokeColor,
-          strokeWidth: element.strokeWidth,
-          roughness: element.roughness,
           seed: element.seed,
+          roughness: element.roughness,
         };
 
         // points array can be empty in the beginning, so it is important to add
@@ -257,6 +298,13 @@ function generateElement(
         // add lines only in arrow
         if (element.type === "arrow") {
           const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element, shape);
+          // for dotted arrows caps, reduce gap to make it more legible
+          if (element.strokeStyle === "dotted") {
+            options.strokeLineDash = [3, 4];
+            // for solid/dashed, keep solid arrow cap
+          } else {
+            delete options.strokeLineDash;
+          }
           shape.push(
             ...[
               generator.line(x3, y3, x2, y2, options),

+ 1 - 0
src/scene/export.ts

@@ -165,6 +165,7 @@ function getWatermarkElement(maxX: number, maxY: number) {
     backgroundColor: "transparent",
     fillStyle: "hachure",
     strokeWidth: 1,
+    strokeStyle: "solid",
     roughness: 1,
     opacity: 100,
   });

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

@@ -25,6 +25,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
   "version": 3,
@@ -49,6 +50,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "diamond",
   "version": 2,
@@ -73,6 +75,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "ellipse",
   "version": 2,
@@ -106,6 +109,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
   "version": 3,
@@ -130,6 +134,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 2,

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

@@ -12,6 +12,7 @@ Object {
   "roughness": 1,
   "seed": 2019559783,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 4,
@@ -34,6 +35,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 5,
@@ -56,6 +58,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 3,

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

@@ -30,6 +30,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
   "version": 7,
@@ -70,6 +71,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
   "version": 7,

File diff ditekan karena terlalu besar
+ 126 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap


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

@@ -12,6 +12,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 3,
@@ -34,6 +35,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 3,

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

@@ -23,6 +23,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "arrow",
   "version": 3,
@@ -56,6 +57,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "line",
   "version": 3,
@@ -78,6 +80,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "diamond",
   "version": 2,
@@ -100,6 +103,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "ellipse",
   "version": 2,
@@ -122,6 +126,7 @@ Object {
   "roughness": 1,
   "seed": 337897,
   "strokeColor": "#000000",
+  "strokeStyle": "solid",
   "strokeWidth": 1,
   "type": "rectangle",
   "version": 2,

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

@@ -36,6 +36,7 @@ function populateElements(
       backgroundColor: h.state.currentItemBackgroundColor,
       fillStyle: h.state.currentItemFillStyle,
       strokeWidth: h.state.currentItemStrokeWidth,
+      strokeStyle: h.state.currentItemStrokeStyle,
       roughness: h.state.currentItemRoughness,
       opacity: h.state.currentItemOpacity,
     });

+ 2 - 0
src/types.ts

@@ -4,6 +4,7 @@ import {
   NonDeletedExcalidrawElement,
   NonDeleted,
   TextAlign,
+  ExcalidrawElement,
 } from "./element/types";
 import { SHAPES } from "./shapes";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -30,6 +31,7 @@ export type AppState = {
   currentItemBackgroundColor: string;
   currentItemFillStyle: string;
   currentItemStrokeWidth: number;
+  currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
   currentItemRoughness: number;
   currentItemOpacity: number;
   currentItemFont: string;

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini