From a0207bf861908da9235a1752a723e69ecdbba734 Mon Sep 17 00:00:00 2001
From: vkalev <vjk1883@gmail.com>
Date: Tue, 6 Jul 2021 12:45:37 -0500
Subject: refactoring

---
 src/client/views/InkControls.tsx | 118 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 118 insertions(+)
 create mode 100644 src/client/views/InkControls.tsx

(limited to 'src/client/views/InkControls.tsx')

diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
new file mode 100644
index 000000000..eeddfce4c
--- /dev/null
+++ b/src/client/views/InkControls.tsx
@@ -0,0 +1,118 @@
+import React = require("react");
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { setupMoveUpEvents, emptyFunction } from "../../Utils";
+import { UndoManager } from "../util/UndoManager";
+import { ControlPoint, InkData, PointData } from "../../fields/InkField";
+import { Transform } from "../util/Transform";
+
+export interface InkControlProps {
+    data: InkData;
+    addedPoints: PointData[];
+    format: number[];
+    ScreenToLocalTransform: () => Transform;
+}
+
+@observer
+export class InkControls extends React.Component<InkControlProps> {
+    // @observable private _controlPoints: ControlPoint[] = [];
+    @observable private _overControl = -1;
+    @observable private _overAddPoint = -1;
+
+    /**
+     * Handles the movement of a selected control point when the user clicks and drags.
+     * @param controlNum The index of the currently selected control point.
+     */
+    @action
+    onControlDown = (e: React.PointerEvent, controlNum: number): void => {
+        if (InkStrokeProperties.Instance) {
+            InkStrokeProperties.Instance.moveControl(0, 0, 1);
+            const controlUndo = UndoManager.StartBatch("DocDecs set radius");
+            const screenScale = this.props.ScreenToLocalTransform().Scale;
+            setupMoveUpEvents(this, e,
+                (e: PointerEvent, down: number[], delta: number[]) => {
+                    InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlNum);
+                    return false;
+                },
+                () => controlUndo?.end(), emptyFunction);
+        }
+    }
+
+    /**
+     * Deletes the currently selected point.
+     */
+    @action
+    onDelete = (e: KeyboardEvent) => {
+        if (["-", "Backspace", "Delete"].includes(e.key)) {
+            if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation();
+        }
+    }
+
+    /**
+     * Changes the current selected control point.
+     */
+    @action
+    changeCurrPoint = (i: number) => {
+        if (InkStrokeProperties.Instance) {
+            InkStrokeProperties.Instance._currentPoint = i;
+            document.addEventListener("keydown", this.onDelete, true);
+        }
+    }
+
+    @action onEnterControl = (i: number) => { this._overControl = i; };
+    @action onLeaveControl = () => { this._overControl = -1; };
+    @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
+    @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
+ 
+    render() {
+        const data = this.props.data;
+        const controlPoints: ControlPoint[] = [];
+        if (data.length >= 4) {
+            for (let i = 0; i <= data.length - 4; i += 4) {
+                controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i });
+                controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 });
+            }
+        }
+        const addedPoints = this.props.addedPoints;
+        const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format;
+
+        return (
+            <>
+                {addedPoints.map((pts, i) =>
+                    <svg height="10" width="10" key={`add${i}`}>
+                        <circle 
+                            cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} 
+                            cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} 
+                            r={strokeWidth / 2} 
+                            stroke={this._overAddPoint === i ? "#1F85DE" : "transparent"} 
+                            strokeWidth={dotsize / 4} fill={this._overAddPoint === i ? "#1F85DE" : "transparent"}
+                            onPointerDown={() => { InkStrokeProperties.Instance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} 
+                            onMouseEnter={() => this.onEnterAddPoint(i)} 
+                            onMouseLeave={this.onLeaveAddPoint} 
+                            pointerEvents="all" 
+                            cursor="all-scroll"
+                        />
+                    </svg>
+                )}
+                {controlPoints.map((control, i) =>
+                    <svg height="10" width="10" key={`ctrl${i}`}>
+                        <rect 
+                            x={(control.X - left - strokeWidth / 2) * scaleX} 
+                            y={(control.Y - top - strokeWidth / 2) * scaleY} 
+                            height={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} 
+                            width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} 
+                            strokeWidth={strokeWidth / 6} stroke="#1F85DE" 
+                            fill={InkStrokeProperties.Instance?._currentPoint === control.I ? "#1F85DE" : "white"}
+                            onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} 
+                            onMouseEnter={() => this.onEnterControl(i)} 
+                            onMouseLeave={this.onLeaveControl} 
+                            pointerEvents="all" 
+                            cursor="default"
+                        />
+                    </svg>
+                )}
+            </>
+        );
+    }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 8bc17cecdfce184e5a426dc2332d3c9ad0406f58 Mon Sep 17 00:00:00 2001
From: vkalev <vjk1883@gmail.com>
Date: Sat, 10 Jul 2021 11:13:42 -0500
Subject: fixed adding point bug with breaking handle tangency

---
 src/client/documents/Documents.ts                  |   8 +-
 src/client/views/GestureOverlay.tsx                |   2 +-
 src/client/views/InkControls.tsx                   |  18 +-
 src/client/views/InkHandles.tsx                    |  27 +--
 src/client/views/InkStroke.tsx                     | 187 ---------------------
 src/client/views/InkStrokeProperties.ts            |  69 ++++++--
 src/client/views/InkingStroke.tsx                  | 187 +++++++++++++++++++++
 src/client/views/PropertiesButtons.tsx             |   4 +-
 src/client/views/collections/CollectionMenu.tsx    |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   4 +-
 src/client/views/nodes/ColorBox.tsx                |   2 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   4 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 14 files changed, 285 insertions(+), 237 deletions(-)
 delete mode 100644 src/client/views/InkStroke.tsx
 create mode 100644 src/client/views/InkingStroke.tsx

(limited to 'src/client/views/InkControls.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 188fe31c0..24682cbd0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -31,7 +31,7 @@ import { CollectionView, CollectionViewType } from "../views/collections/Collect
 import { ContextMenu } from "../views/ContextMenu";
 import { ContextMenuProps } from "../views/ContextMenuItem";
 import { DFLT_IMAGE_NATIVE_DIM } from "../views/globalCssVariables.scss";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkStroke } from "../views/InkStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke";
 import { AudioBox } from "../views/nodes/AudioBox";
 import { ColorBox } from "../views/nodes/ColorBox";
 import { ComparisonBox } from "../views/nodes/ComparisonBox";
@@ -432,7 +432,7 @@ export namespace Docs {
                 options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true }
             }],
             [DocumentType.INK, {
-                layout: { view: InkStroke, dataField: defaultDataKey },
+                layout: { view: InkingStroke, dataField: defaultDataKey },
                 options: { _fontFamily: "cursive", backgroundColor: "transparent", links: ComputedField.MakeFunction("links(self)") as any }
             }],
             [DocumentType.SCREENSHOT, {
@@ -760,7 +760,7 @@ export namespace Docs {
             const I = new Doc();
             I[Initializing] = true;
             I.type = DocumentType.INK;
-            I.layout = InkStroke.LayoutString("data");
+            I.layout = InkingStroke.LayoutString("data");
             I.color = color;
             I.fillColor = fillColor;
             I.strokeWidth = Number(strokeWidth);
@@ -1144,7 +1144,7 @@ export namespace DocUtils {
             layout = AudioBox.LayoutString;
         } else if (field instanceof InkField) {
             created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved);
-            layout = InkStroke.LayoutString;
+            layout = InkingStroke.LayoutString;
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
             layout = CollectionView.LayoutString;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 455b3a935..491bf18b2 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -17,7 +17,7 @@ import { Scripting } from "../util/Scripting";
 import { Transform } from "../util/Transform";
 import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
 import "./GestureOverlay.scss";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
 import HorizontalPalette from "./Palette";
diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index eeddfce4c..4d8b2c6b5 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -22,17 +22,17 @@ export class InkControls extends React.Component<InkControlProps> {
 
     /**
      * Handles the movement of a selected control point when the user clicks and drags.
-     * @param controlNum The index of the currently selected control point.
+     * @param controlIndex The index of the currently selected control point.
      */
     @action
-    onControlDown = (e: React.PointerEvent, controlNum: number): void => {
+    onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
         if (InkStrokeProperties.Instance) {
             InkStrokeProperties.Instance.moveControl(0, 0, 1);
             const controlUndo = UndoManager.StartBatch("DocDecs set radius");
             const screenScale = this.props.ScreenToLocalTransform().Scale;
             setupMoveUpEvents(this, e,
                 (e: PointerEvent, down: number[], delta: number[]) => {
-                    InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlNum);
+                    InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
                     return false;
                 },
                 () => controlUndo?.end(), emptyFunction);
@@ -60,12 +60,20 @@ export class InkControls extends React.Component<InkControlProps> {
         }
     }
 
+    /**
+     * Updates whether a user has hovered over a particular control point or point that could be added
+     * on click.
+     */
     @action onEnterControl = (i: number) => { this._overControl = i; };
     @action onLeaveControl = () => { this._overControl = -1; };
     @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; };
     @action onLeaveAddPoint = () => { this._overAddPoint = -1; };
  
     render() {
+        const formatInstance = InkStrokeProperties.Instance;
+        if (!formatInstance) return (null);
+
+        // Accessing the current ink's data and extracting all control points.
         const data = this.props.data;
         const controlPoints: ControlPoint[] = [];
         if (data.length >= 4) {
@@ -87,7 +95,7 @@ export class InkControls extends React.Component<InkControlProps> {
                             r={strokeWidth / 2} 
                             stroke={this._overAddPoint === i ? "#1F85DE" : "transparent"} 
                             strokeWidth={dotsize / 4} fill={this._overAddPoint === i ? "#1F85DE" : "transparent"}
-                            onPointerDown={() => { InkStrokeProperties.Instance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} 
+                            onPointerDown={() => { formatInstance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} 
                             onMouseEnter={() => this.onEnterAddPoint(i)} 
                             onMouseLeave={this.onLeaveAddPoint} 
                             pointerEvents="all" 
@@ -103,7 +111,7 @@ export class InkControls extends React.Component<InkControlProps> {
                             height={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} 
                             width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} 
                             strokeWidth={strokeWidth / 6} stroke="#1F85DE" 
-                            fill={InkStrokeProperties.Instance?._currentPoint === control.I ? "#1F85DE" : "white"}
+                            fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"}
                             onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} 
                             onMouseEnter={() => this.onEnterControl(i)} 
                             onMouseLeave={this.onLeaveControl} 
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index 993e427b3..a33380221 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -15,33 +15,38 @@ export interface InkControlProps {
 
 @observer
 export class InkHandles extends React.Component<InkControlProps> {
-    @observable private _brokenIndices: number[] = [];
-
     /**
      * Handles the movement of a selected handle point when the user clicks and drags.
      * @param handleNum The index of the currently selected handle point.
      */
-     onHandleDown = (e: React.PointerEvent, handleNum: number): void => {
+     onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
         if (InkStrokeProperties.Instance) {
             InkStrokeProperties.Instance.moveControl(0, 0, 1);
             const controlUndo = UndoManager.StartBatch("DocDecs set radius");
             const screenScale = this.props.ScreenToLocalTransform().Scale;
-            document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, handleNum), true);
+
+            const order = handleIndex % 4;
+            const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
+            const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2;
+            document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, controlIndex), true);
             setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
-                    InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum, this._brokenIndices);
+                    InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
                     return false;
                 }, () => controlUndo?.end(), emptyFunction
             );
         }
     }
 
+    /**
+     * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and 
+     * its matching (opposite) handle to a list of broken handle indices.
+     * @param handleNum The index of the currently selected handle point.
+     */
     @action
-    onBreakTangent = (e: KeyboardEvent, handleIndex: number) => {
+    onBreakTangent = (e: KeyboardEvent, controlIndex: number) => {
         if (["Alt"].includes(e.key)) {
-            const order = handleIndex % 4;
-            const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
-            if (!this._brokenIndices.includes(handleIndex) && !this._brokenIndices.includes(oppositeHandleIndex)) {
-                this._brokenIndices.push(handleIndex, oppositeHandleIndex);
+            if (!InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
+                InkStrokeProperties.Instance?._brokenIndices.push(controlIndex);
             }
         }
     }
@@ -49,6 +54,8 @@ export class InkHandles extends React.Component<InkControlProps> {
     render() {
         const formatInstance = InkStrokeProperties.Instance;
         if (!formatInstance) return (null);
+
+        // Accessing the current ink's data and extracting all handle points and handle lines.
         const data = this.props.data;
         const handlePoints: HandlePoint[] = [];
         const handleLines: HandleLine[] = [];
diff --git a/src/client/views/InkStroke.tsx b/src/client/views/InkStroke.tsx
deleted file mode 100644
index 9ca8bdbea..000000000
--- a/src/client/views/InkStroke.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-import React = require("react");
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { documentSchema } from "../../fields/documentSchemas";
-import { InkData, InkField, InkTool, ControlPoint, HandlePoint, HandleLine } from "../../fields/InkField";
-import { makeInterface } from "../../fields/Schema";
-import { Cast, StrCast } from "../../fields/Types";
-import { TraceMobx } from "../../fields/util";
-import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils";
-import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { InteractionUtils } from "../util/InteractionUtils";
-import { Scripting } from "../util/Scripting";
-import { UndoManager } from "../util/UndoManager";
-import { ContextMenu } from "./ContextMenu";
-import { ViewBoxBaseComponent } from "./DocComponent";
-import "./InkStroke.scss";
-import { FieldView, FieldViewProps } from "./nodes/FieldView";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { InkControls } from "./InkControls";
-import { InkHandles } from "./InkHandles";
-
-type InkDocument = makeInterface<[typeof documentSchema]>;
-const InkDocument = makeInterface(documentSchema);
-
-@observer
-export class InkStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
-    static readonly MaskDim = 50000;
-    @observable private _properties?: InkStrokeProperties;
-
-    constructor(props: FieldViewProps & InkDocument) {
-        super(props);
-
-        this._properties = InkStrokeProperties.Instance;
-    }
-
-    public static LayoutString(fieldStr: string) { 
-        return FieldView.LayoutString(InkStroke, fieldStr); 
-    }
-
-    analyzeStrokes() {
-        const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
-        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
-    }
-
-    @action
-    public static toggleMask = (inkDoc: Doc) => {
-        inkDoc.isInkMask = !inkDoc.isInkMask;
-        inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
-        inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined;
-        inkDoc.color = "#9b9b9bff";
-        inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
-    }
-
-    /**
-     * Handles the movement of the entire ink object when the user clicks and drags.
-     * @param e React Pointer Event.
-     */
-    onPointerDown = (e: React.PointerEvent) => {
-        if (this.props.isSelected(true)) {
-            setupMoveUpEvents(this, e, returnFalse, emptyFunction, 
-                action((e: PointerEvent, doubleTap: boolean | undefined) =>
-                doubleTap && this._properties && (this._properties._controlButton = true))
-            );
-        }
-    }
-
-    render() {
-        TraceMobx();
-
-        const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
-        const strokeWidth = Number(this.layoutDoc.strokeWidth);
-        const lineTop = Math.min(...data.map(p => p.Y));
-        const lineBottom = Math.max(...data.map(p => p.Y));
-        const lineLeft = Math.min(...data.map(p => p.X));
-        const lineRight = Math.max(...data.map(p => p.X));
-        const left = lineLeft - strokeWidth / 2;
-        const top = lineTop - strokeWidth / 2;
-        const right = lineRight + strokeWidth / 2;
-        const bottom = lineBottom + strokeWidth / 2;
-        const width = Math.max(1, right - left);
-        const height = Math.max(1, bottom - top);
-        const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth);
-        const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth);
-        const strokeColor = StrCast(this.layoutDoc.color, "");
-        const dotsize = Math.max(width * scaleX, height * scaleY) / 40;
-
-        // Visually renders the polygonal line made by the user.
-        const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, 
-            StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), 
-            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), 
-            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
-            this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, 
-            false);
-        const selectedLine = InteractionUtils.CreatePolyline(
-            data, lineLeft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, 
-            strokeWidth / 6,StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
-            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
-            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
-            this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, 
-            false);
-        // Invisible polygonal line that enables the ink to be selected by the user.
-        const clickableLine = InteractionUtils.CreatePolyline(data, left, top, 
-            this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, 
-            strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), 
-            StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", 
-            this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true);
-        const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth,
-            StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
-            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
-            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
-            this.props.isSelected() && strokeWidth <= 5, false);
-
-        return (
-            <svg className="inkStroke"
-                style={{
-                    pointerEvents: this.props.Document.isInkMask && this.props.layerProvider?.(this.props.Document) !== false ? "all" : "none",
-                    transform: this.props.Document.isInkMask ? `translate(${InkStroke.MaskDim / 2}px, ${InkStroke.MaskDim / 2}px)` : undefined,
-                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                    overflow: "visible",
-                }}
-                onPointerDown={this.onPointerDown}
-                onContextMenu={() => {
-                    const cm = ContextMenu.Instance;
-                    if (cm) {
-                        !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
-                        cm.addItem({ description: "Toggle Mask", event: () => InkStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
-                        cm.addItem({ description: "Edit Points", event: action(() => {if (this._properties) { this._properties._controlButton = !this._properties._controlButton; }} ), icon: "paint-brush" });
-                    }
-                }}
-            >
-                
-                {clickableLine}
-                {inkLine}
-                {this.props.isSelected() ? selectedLine : ""}
-                {this.props.isSelected() && this._properties?._controlButton ? 
-                    <>
-                        <InkControls 
-                            data={data} 
-                            addedPoints={addedPoints} 
-                            format={[left, top, scaleX, scaleY, strokeWidth, dotsize]} 
-                            ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
-                        <InkHandles 
-                            data={data} 
-                            format={[left, top, scaleX, scaleY, strokeWidth, dotsize]} 
-                            ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> 
-                    </> : ""}
-            </svg>
-        );
-    }
-}
-
-
-export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); }
-export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); }
-export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); }
-export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); }
-export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); }
-export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); }
-export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
-export function ActiveInkPen(): Doc { return Doc.UserDoc(); }
-export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
-export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
-export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
-export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
-export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
-export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
-export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
-Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
-    CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None;
-    SetActiveInkWidth(width);
-    SetActiveInkColor(color);
-    SetActiveFillColor(fill);
-    SetActiveArrowStart(arrowStart);
-    SetActiveArrowEnd(arrowEnd);
-    SetActiveDash(dash);
-});
-Scripting.addGlobal(function activateEraser(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Eraser : InkTool.None; });
-Scripting.addGlobal(function activateStamp(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Stamp : InkTool.None; });
-Scripting.addGlobal(function deactivateInk() { return CurrentUserUtils.SelectedTool = InkTool.None; });
-Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); });
-Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
-Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); });
-Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); });
-Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); });
-Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); });
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 812e8ff6e..4ec03c560 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,7 +1,7 @@
 import { action, computed, observable } from "mobx";
 import { Doc, Field, Opt } from "../../fields/Doc";
 import { Document } from "../../fields/documentSchemas";
-import { InkField, InkData, PointData } from "../../fields/InkField";
+import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField";
 import { Cast, NumCast } from "../../fields/Types";
 import { DocumentType } from "../documents/DocumentTypes";
 import { SelectionManager } from "../util/SelectionManager";
@@ -13,6 +13,7 @@ export class InkStrokeProperties {
     @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
+    @observable _brokenIndices: number[] = [];
 
     constructor() {
         InkStrokeProperties.Instance = this;
@@ -73,6 +74,7 @@ export class InkStrokeProperties {
     addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => {
         this.selectedInk?.forEach(action(inkView => {
             if (this.selectedInk?.length === 1) {
+                const newPoint = { X: x, Y: y };
                 const doc = Document(inkView.rootDoc);
                 if (doc.type === DocumentType.INK) {
                     const ink = Cast(doc.data, InkField)?.inkData;
@@ -88,12 +90,25 @@ export class InkStrokeProperties {
                         for (var i = 0; i < spNum; i++) {
                             ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
                         }
-                        for (var j = 0; j < 4; j++) {
-                            newPoints.push({ X: x, Y: y });
 
-                        }
+                        // Updating the indices of the control points whose handle tangency has been broken.
+                        this._brokenIndices = this._brokenIndices.map((control) => { 
+                            if (control >= spNum) {
+                                return control + 4; 
+                            } else {
+                                return control;
+                            }
+                        });
+
+                        // const [handleA, handleB] = this.getNewHandlePoints(newPoint, pts[index-1], pts[index+1]);
+                        newPoints.push(newPoint);
+                        newPoints.push(newPoint);
+                        newPoints.push(newPoint);
+                        newPoints.push(newPoint);
+
                         for (var i = spNum; i < ink.length; i++) {
                             newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+
                         }
                         this._currentPoint = -1;
                         Doc.GetProto(doc).data = new InkField(newPoints);
@@ -103,6 +118,20 @@ export class InkStrokeProperties {
         }));
     }
 
+    getNewHandlePoints = (newControl: PointData, a: PointData, b: PointData) => {
+        // find midpoint between the left and right control point of new control
+        // rotate midpoint by +-pi/2 to get new handle points
+        // multiplying x-y coordinates of both by 10/L where L is its current magnitude
+        const angle = this.angleChange(a, b, newControl);
+        const midpoint = this.rotatePoint(a, newControl, angle/2);
+        // const handleA = this.rotatePoint(midpoint, newControl, -Math.PI/2);
+        // const handleB = this.rotatePoint(midpoint, newControl, -Math.PI/2);
+        const handleA = { X: midpoint.X + (20 * Math.cos(-Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(-Math.PI/2)) };
+        const handleB = { X: midpoint.X + (20 * Math.cos(Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(Math.PI/2)) };
+
+        return [handleA, handleB];
+    }
+
     /**
      * Deletes the points of the current ink instance.
      * @returns The changed x- and y-coordinates of the control points.
@@ -195,23 +224,27 @@ export class InkStrokeProperties {
         return target;
     }
 
+    angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
+        const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
+        const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
+        // Normalizing the vectors.
+        vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA };
+        vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB };
+        const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y;
+        return Math.acos(dotProduct);
+    }
+
     /**
      * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin.
      */
     angleChange = (a: PointData, b: PointData, origin: PointData) => {
         // Finding vector representation of inputted points relative to new origin.
-        let vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y };
-        let vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y };
+        const vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y };
+        const vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y };
         const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X;
         // Determining whether rotation is clockwise or counterclockwise.
         const sign = crossProduct < 0 ? 1 : -1;
-        const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
-        const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
-        // Normalizing the vectors.
-        vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA };
-        vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB };
-        const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y;
-        const theta = Math.acos(dotProduct);
+        const theta = this.angleBetweenTwoVectors(vectorA, vectorB);
         return sign * theta;
     }
 
@@ -220,20 +253,20 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    moveHandle = (deltaX: number, deltaY: number, handleIndex: number, brokenIndices: number[]) =>
+    moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
             const order = handleIndex % 4;
             const oldHandlePoint = ink[handleIndex];
+            let oppositeHandlePoint = ink[oppositeHandleIndex];
+            const controlPoint = ink[controlIndex];
             const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
             ink[handleIndex] = newHandlePoint;
             
             // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-            if (!brokenIndices.includes(handleIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
-                let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3];
-                const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1];
+            if (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
                 const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
                 oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
-                order === 1 ? ink[handleIndex - 3] = oppositeHandlePoint : ink[handleIndex + 3] = oppositeHandlePoint;
+                ink[oppositeHandleIndex] = oppositeHandlePoint;
             }
 
             return ink;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
new file mode 100644
index 000000000..b630c20cc
--- /dev/null
+++ b/src/client/views/InkingStroke.tsx
@@ -0,0 +1,187 @@
+import React = require("react");
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../fields/Doc";
+import { documentSchema } from "../../fields/documentSchemas";
+import { InkData, InkField, InkTool } from "../../fields/InkField";
+import { makeInterface } from "../../fields/Schema";
+import { Cast, StrCast } from "../../fields/Types";
+import { TraceMobx } from "../../fields/util";
+import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { InteractionUtils } from "../util/InteractionUtils";
+import { Scripting } from "../util/Scripting";
+import { ContextMenu } from "./ContextMenu";
+import { ViewBoxBaseComponent } from "./DocComponent";
+import "./InkStroke.scss";
+import { FieldView, FieldViewProps } from "./nodes/FieldView";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
+import { InkControls } from "./InkControls";
+import { InkHandles } from "./InkHandles";
+
+type InkDocument = makeInterface<[typeof documentSchema]>;
+const InkDocument = makeInterface(documentSchema);
+
+@observer
+export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+    static readonly MaskDim = 50000;
+    @observable private _properties?: InkStrokeProperties;
+
+    constructor(props: FieldViewProps & InkDocument) {
+        super(props);
+
+        this._properties = InkStrokeProperties.Instance;
+    }
+
+    public static LayoutString(fieldStr: string) { 
+        return FieldView.LayoutString(InkingStroke, fieldStr); 
+    }
+
+    analyzeStrokes() {
+        const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
+    }
+
+    @action
+    public static toggleMask = (inkDoc: Doc) => {
+        inkDoc.isInkMask = !inkDoc.isInkMask;
+        inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
+        inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined;
+        inkDoc.color = "#9b9b9bff";
+        inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
+    }
+
+    /**
+     * Handles the movement of the entire ink object when the user clicks and drags.
+     */
+    onPointerDown = (e: React.PointerEvent) => {
+        if (this.props.isSelected(true)) {
+            setupMoveUpEvents(this, e, returnFalse, emptyFunction, 
+                action((e: PointerEvent, doubleTap: boolean | undefined) =>
+                doubleTap && this._properties && (this._properties._controlButton = true))
+            );
+        }
+    }
+
+    render() {
+        TraceMobx();
+        // Extracting the ink data and formatting information of the current ink stroke.
+        const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
+        const strokeWidth = Number(this.layoutDoc.strokeWidth);
+        const lineTop = Math.min(...data.map(p => p.Y));
+        const lineBottom = Math.max(...data.map(p => p.Y));
+        const lineLeft = Math.min(...data.map(p => p.X));
+        const lineRight = Math.max(...data.map(p => p.X));
+        const left = lineLeft - strokeWidth / 2;
+        const top = lineTop - strokeWidth / 2;
+        const right = lineRight + strokeWidth / 2;
+        const bottom = lineBottom + strokeWidth / 2;
+        const width = Math.max(1, right - left);
+        const height = Math.max(1, bottom - top);
+        const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth);
+        const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth);
+        const strokeColor = StrCast(this.layoutDoc.color, "");
+        const dotsize = Math.max(width * scaleX, height * scaleY) / 40;
+
+        // Visually renders the polygonal line made by the user.
+        const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, 
+            StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), 
+            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), 
+            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
+            this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, 
+            false);
+        // Thin blue line indicating that the current ink stroke is selected.
+        const selectedLine = InteractionUtils.CreatePolyline(
+            data, lineLeft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, 
+            strokeWidth / 6,StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
+            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
+            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
+            this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, 
+            false);
+        // Invisible polygonal line that enables the ink to be selected by the user.
+        const clickableLine = InteractionUtils.CreatePolyline(data, left, top, 
+            this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, 
+            strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), 
+            StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", 
+            this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true);
+        // Set of points rendered upon the ink that can be added if a user clicks on one.
+        const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth,
+            StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
+            StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker),
+            StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 
+            this.props.isSelected() && strokeWidth <= 5, false);
+
+        return (
+            <svg className="inkStroke"
+                style={{
+                    pointerEvents: this.props.Document.isInkMask && this.props.layerProvider?.(this.props.Document) !== false ? "all" : "none",
+                    transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
+                    mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
+                    overflow: "visible",
+                }}
+                onPointerDown={this.onPointerDown}
+                onContextMenu={() => {
+                    const cm = ContextMenu.Instance;
+                    if (cm) {
+                        !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
+                        cm.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
+                        cm.addItem({ description: "Edit Points", event: action(() => {if (this._properties) { this._properties._controlButton = !this._properties._controlButton; }} ), icon: "paint-brush" });
+                    }
+                }}
+            >
+                
+                {clickableLine}
+                {inkLine}
+                {this.props.isSelected() ? selectedLine : ""}
+                {this.props.isSelected() && this._properties?._controlButton ? 
+                    <>
+                        <InkControls 
+                            data={data} 
+                            addedPoints={addedPoints} 
+                            format={[left, top, scaleX, scaleY, strokeWidth, dotsize]} 
+                            ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
+                        <InkHandles 
+                            data={data} 
+                            format={[left, top, scaleX, scaleY, strokeWidth, dotsize]} 
+                            ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> 
+                    </> : ""}
+            </svg>
+        );
+    }
+}
+
+
+export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); }
+export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); }
+export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); }
+export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); }
+export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); }
+export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); }
+export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
+export function ActiveInkPen(): Doc { return Doc.UserDoc(); }
+export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
+export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
+export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
+export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
+export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
+export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); }
+export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) {
+    CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None;
+    SetActiveInkWidth(width);
+    SetActiveInkColor(color);
+    SetActiveFillColor(fill);
+    SetActiveArrowStart(arrowStart);
+    SetActiveArrowEnd(arrowEnd);
+    SetActiveDash(dash);
+});
+Scripting.addGlobal(function activateEraser(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Eraser : InkTool.None; });
+Scripting.addGlobal(function activateStamp(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Stamp : InkTool.None; });
+Scripting.addGlobal(function deactivateInk() { return CurrentUserUtils.SelectedTool = InkTool.None; });
+Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); });
+Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); });
+Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); });
+Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); });
+Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); });
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 920244463..5c41a96d0 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -11,7 +11,7 @@ import { DocumentType } from '../documents/DocumentTypes';
 import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { CollectionViewType } from './collections/CollectionView';
-import { InkStroke } from './InkStroke';
+import { InkingStroke } from './InkingStroke';
 import { DocumentView } from './nodes/DocumentView';
 import './PropertiesButtons.scss';
 import React = require("react");
@@ -56,7 +56,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         return this.propertyToggleBtn("Dictate", "_showAudio", on => `${on ? "Hide" : "Show"} dictation/recording controls`, on => "microphone");
     }
     @computed get maskButton() {
-        return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkStroke.toggleMask(dv?.layoutDoc || doc));
+        return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc));
     }
     @computed get clustersButton() {
         return this.propertyToggleBtn("Clusters", "_useClusters", on => `${on ? "Hide" : "Show"} clusters`, on => "braille");
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 65061d260..6e6fabd0d 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -25,7 +25,7 @@ import { undoBatch } from "../../util/UndoManager";
 import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu";
 import { EditableView } from "../EditableView";
 import { GestureOverlay } from "../GestureOverlay";
-import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkStroke";
+import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkingStroke";
 import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 import { DocumentView } from "../nodes/DocumentView";
 import { RichTextMenu } from "../nodes/formattedText/RichTextMenu";
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9e81a6cda..accb80c5a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -32,7 +32,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { Timeline } from "../../animationtimeline/Timeline";
 import { ContextMenu } from "../../ContextMenu";
 import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";
 import { LightboxView } from "../../LightboxView";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 744a9c5de..092823603 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -13,7 +13,7 @@ import { SelectionManager } from "../../util/SelectionManager";
 import { Transform } from "../../util/Transform";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { DocComponent } from "../DocComponent";
-import { InkStroke } from "../InkStroke";
+import { InkingStroke } from "../InkingStroke";
 import { StyleProp } from "../StyleProvider";
 import "./CollectionFreeFormDocumentView.scss";
 import { DocumentView, DocumentViewProps } from "./DocumentView";
@@ -38,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     @observable _animPos: number[] | undefined = undefined;
     @observable _contentView: DocumentView | undefined | null;
     get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive
-    get maskCentering() { return this.props.Document.isInkMask ? InkStroke.MaskDim / 2 : 0; }
+    get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; }
     get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.props.jitterRotation}deg)`; }
     get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
     get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 2d466763b..8da5cd1b1 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -12,7 +12,7 @@ import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SelectionManager } from "../../util/SelectionManager";
 import { undoBatch } from "../../util/UndoManager";
 import { ViewBoxBaseComponent } from "../DocComponent";
-import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from "../InkStroke";
+import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke";
 import "./ColorBox.scss";
 import { FieldView, FieldViewProps } from './FieldView';
 import { RichTextMenu } from "./formattedText/RichTextMenu";
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 5da3a52d5..f0a54e4ac 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -10,7 +10,7 @@ import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
 import { CollectionView } from "../collections/CollectionView";
-import { InkStroke } from "../InkStroke";
+import { InkingStroke } from "../InkingStroke";
 import { PresElementBox } from "../presentationview/PresElementBox";
 import { SearchBox } from "../search/SearchBox";
 import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
@@ -224,7 +224,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
                     FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView,
                     CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
                     PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
-                    ColorBox, DashWebRTCVideo, LinkAnchorBox, InkStroke, LinkBox, ScriptingBox,
+                    ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox,
                     ScreenshotBox,
                     HTMLtag, ComparisonBox
                 }}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 924b0e7d3..b861669f8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -34,7 +34,7 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from "../DocComponent";
 import { EditableView } from '../EditableView';
-import { InkStroke } from "../InkStroke";
+import { InkingStroke } from "../InkingStroke";
 import { LightboxView } from "../LightboxView";
 import { StyleLayers, StyleProp } from "../StyleProvider";
 import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
@@ -1138,8 +1138,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     render() {
         TraceMobx();
-        const xshift = () => (this.props.Document.isInkMask ? InkStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
-        const yshift = () => (this.props.Document.isInkMask ? InkStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
+        const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
+        const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
         return (<div className="contentFittingDocumentView">
             {!this.props.Document || !this.props.PanelWidth() ? (null) : (
                 <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
-- 
cgit v1.2.3-70-g09d2


From 48620bbe25f92eb179d53846aae5f0164ca6f1c2 Mon Sep 17 00:00:00 2001
From: vkalev <vjk1883@gmail.com>
Date: Thu, 15 Jul 2021 01:27:43 -0500
Subject: adding new point creates tangent handle lines + snapping handle
 tangents not working

---
 src/client/views/InkControls.tsx        |  12 ++-
 src/client/views/InkHandles.tsx         |   1 -
 src/client/views/InkStrokeProperties.ts | 170 +++++++++++++++++++++-----------
 src/fields/InkField.ts                  |   3 -
 4 files changed, 121 insertions(+), 65 deletions(-)

(limited to 'src/client/views/InkControls.tsx')

diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index 4d8b2c6b5..da7b0df16 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -30,12 +30,18 @@ export class InkControls extends React.Component<InkControlProps> {
             InkStrokeProperties.Instance.moveControl(0, 0, 1);
             const controlUndo = UndoManager.StartBatch("DocDecs set radius");
             const screenScale = this.props.ScreenToLocalTransform().Scale;
+            const order = controlIndex % 4;
+            const handleIndexA  = order === 2 ? controlIndex - 1 : controlIndex - 2;
+            const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1;
             setupMoveUpEvents(this, e,
                 (e: PointerEvent, down: number[], delta: number[]) => {
                     InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
                     return false;
                 },
-                () => controlUndo?.end(), emptyFunction);
+                () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) =>
+                { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
+                    InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                }}));
         }
     }
 
@@ -112,7 +118,9 @@ export class InkControls extends React.Component<InkControlProps> {
                             width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} 
                             strokeWidth={strokeWidth / 6} stroke="#1F85DE" 
                             fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"}
-                            onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} 
+                            onPointerDown={(e) => { 
+                                this.changeCurrPoint(control.I); 
+                                this.onControlDown(e, control.I); }} 
                             onMouseEnter={() => this.onEnterControl(i)} 
                             onMouseLeave={this.onLeaveControl} 
                             pointerEvents="all" 
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index a33380221..ba3fdf9db 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -24,7 +24,6 @@ export class InkHandles extends React.Component<InkControlProps> {
             InkStrokeProperties.Instance.moveControl(0, 0, 1);
             const controlUndo = UndoManager.StartBatch("DocDecs set radius");
             const screenScale = this.props.ScreenToLocalTransform().Scale;
-
             const order = handleIndex % 4;
             const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
             const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2;
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 4ec03c560..a3f7562e0 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -71,70 +71,109 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => {
-        this.selectedInk?.forEach(action(inkView => {
-            if (this.selectedInk?.length === 1) {
-                const newPoint = { X: x, Y: y };
-                const doc = Document(inkView.rootDoc);
-                if (doc.type === DocumentType.INK) {
-                    const ink = Cast(doc.data, InkField)?.inkData;
-                    if (ink) {
-                        const newPoints: { X: number, Y: number }[] = [];
-                        var counter = 0;
-                        for (var k = 0; k < index; k++) {
-                            control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++);
-                        }
-                        //decide where to put the new coordinate
-                        const spNum = Math.floor(counter / 2) * 4 + 2;
-
-                        for (var i = 0; i < spNum; i++) {
-                            ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
-                        }
-
-                        // Updating the indices of the control points whose handle tangency has been broken.
-                        this._brokenIndices = this._brokenIndices.map((control) => { 
-                            if (control >= spNum) {
-                                return control + 4; 
-                            } else {
-                                return control;
+    addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => {
+        this.applyFunction((doc: Doc, ink: InkData) => {
+            const newControl = { X: x, Y: y };
+            const newPoints: InkData = [];
+            let [counter, start, end] = [0, 0, 0];
+            for (let k = 0; k < points.length; k++) {
+                if (end === 0) {
+                    controls.forEach((control) => {
+                        if (control.X === points[k].X && control.Y === points[k].Y) {
+                            if (k < index) {
+                                counter++;
+                                start = k;
+                            } else if (k > index) {
+                                end = k;
                             }
-                        });
-
-                        // const [handleA, handleB] = this.getNewHandlePoints(newPoint, pts[index-1], pts[index+1]);
-                        newPoints.push(newPoint);
-                        newPoints.push(newPoint);
-                        newPoints.push(newPoint);
-                        newPoints.push(newPoint);
-
-                        for (var i = spNum; i < ink.length; i++) {
-                            newPoints.push({ X: ink[i].X, Y: ink[i].Y });
-
                         }
-                        this._currentPoint = -1;
-                        Doc.GetProto(doc).data = new InkField(newPoints);
-                    }
+                    });
                 }
             }
-        }));
+            if (end === 0) end = points.length-1;
+            // Index of new control point with regards to the ink data.
+            const newIndex = Math.floor(counter / 2) * 4 + 2;
+            // Creating new ink data with new control point and handle points inputted.
+            for (let i = 0; i < ink.length; i++) {
+                if (i === newIndex) { 
+                    const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index+1), points.slice(index, end), newControl);
+                    newPoints.push(handleA, newControl, newControl, handleB);
+                    const [rightControl, rightHandle]  = [points[end], ink[i]];
+                    const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
+                    rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
+                } else if (i === newIndex - 1) {
+                    const [leftControl, leftHandle] = [points[start], ink[i]];
+                    const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle);
+                    leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y });
+                } else {
+                    ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+                }
+                
+            }
+            // Updating the indices of the control points whose handle tangency has been broken.
+            this._brokenIndices = this._brokenIndices.map((control) => { 
+                if (control >= newIndex) {
+                    return control + 4; 
+                } else {
+                    return control;
+                }
+            });
+            this._currentPoint = -1;
+            return newPoints;
+        });
     }
 
-    getNewHandlePoints = (newControl: PointData, a: PointData, b: PointData) => {
-        // find midpoint between the left and right control point of new control
-        // rotate midpoint by +-pi/2 to get new handle points
-        // multiplying x-y coordinates of both by 10/L where L is its current magnitude
-        const angle = this.angleChange(a, b, newControl);
-        const midpoint = this.rotatePoint(a, newControl, angle/2);
-        // const handleA = this.rotatePoint(midpoint, newControl, -Math.PI/2);
-        // const handleB = this.rotatePoint(midpoint, newControl, -Math.PI/2);
-        const handleA = { X: midpoint.X + (20 * Math.cos(-Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(-Math.PI/2)) };
-        const handleB = { X: midpoint.X + (20 * Math.cos(Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(Math.PI/2)) };
+    /**
+     * Scales a handle point of a control point that is adjacent to a newly added one.
+     * @param isLeft Determines if the current control point is on the left or right side of the newly added one.
+     * @param start Beginning index of curve from the left control point to the newly added one.
+     * @param end Final index of curve from the newly added control point to its right neighbor.
+     */
+    getScaledHandlePoint(isLeft: boolean, start: number, end: number, index: number, control: PointData, handle: PointData) {
+        const prevSize = end - start;
+        const newSize = isLeft ? index - start : end - index;
+        const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y };
+        const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) };
+        return scaledVector;
+    }
 
+    /**
+     * Determines the position of the handle points of a newly added control point by finding the
+     * tangent vectors to the split curve at the new control. Given the properties of Bézier curves,
+     * the tangent vector to a control point is equivalent to the first/last (depending on the direction
+     * of the curve) leg of the Bézier curve's derivative.
+     * (Source: https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html)
+     * 
+     * @param C The curve represented by all points from the previous control until the newly added point.
+     * @param D The curve represented by all points from the newly added point to the next control.
+     * @param newControl The newly added control point.
+     */
+    getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => {
+        const [m, n] = [C.length, D.length];
+        let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2)));
+        let handleSizeB = Math.sqrt((Math.pow(D[n-1].X - newControl.X, 2)) + (Math.pow(D[n-1].Y - newControl.Y, 2)));
+        if (handleSizeA < 75 && handleSizeB < 75) {
+            handleSizeA *= 3;
+            handleSizeB *= 3;
+        }
+        if (Math.abs(handleSizeA - handleSizeB) < 50) {
+            handleSizeA *= 5;
+            handleSizeB *= 5;
+        } else if (Math.abs(handleSizeA - handleSizeB) < 150) {
+            handleSizeA *= 2;
+            handleSizeB *= 2;
+        }
+        // Finding the last leg of the derivative curve of C.
+        const dC = { X: (handleSizeA / n) * (C[m-1].X - C[m-2].X), Y: (handleSizeA / n) * (C[m-1].Y - C[m-2].Y) };
+        // Finding the first leg of the derivative curve of D. 
+        const dD = { X: (handleSizeB / m) * (D[1].X - D[0].X), Y: (handleSizeB / m) * (D[1].Y - D[0].Y) };
+        const handleA = { X: newControl.X - dC.X, Y: newControl.Y - dC.Y };
+        const handleB = { X: newControl.X + dD.X, Y: newControl.Y + dD.Y };
         return [handleA, handleB];
     }
 
     /**
-     * Deletes the points of the current ink instance.
-     * @returns The changed x- and y-coordinates of the control points.
+     * Deletes the current control point of the selected ink instance.
      */
     @undoBatch
     @action
@@ -160,9 +199,8 @@ export class InkStrokeProperties {
     }, true)
 
     /**
-     * Rotates the points of the current ink instance by a certain angle degree.
-     * @param angle The angle at which to rotate the ink (all of its x- and y-coordinates).
-     * @returns The changed x- and y-coordinates of the control points.
+     * Rotates the entire selected ink instance.
+     * @param angle The angle at which to rotate the ink in radians.
      */
     @undoBatch
     @action
@@ -210,6 +248,18 @@ export class InkStrokeProperties {
             return newPoints;
         })
 
+    snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
+        this.applyFunction((doc: Doc, ink: InkData) => {
+            this._brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 1);
+            const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
+            const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
+            const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
+            const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference);
+            ink[handleIndexB] = newHandleB;
+            return ink;
+        });
+    }
+
     /**
      * Rotates the target point about the origin point for a given angle (radians).
      */
@@ -224,6 +274,11 @@ export class InkStrokeProperties {
         return target;
     }
 
+    /**
+     * Finds the angle between two inputted vectors.
+     * 
+     * α = arccos(a·b / |a|·|b|), where a and b are both vectors.
+     */
     angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
         const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
         const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
@@ -255,20 +310,17 @@ export class InkStrokeProperties {
     @action
     moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
         this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
-            const order = handleIndex % 4;
             const oldHandlePoint = ink[handleIndex];
             let oppositeHandlePoint = ink[oppositeHandleIndex];
             const controlPoint = ink[controlIndex];
             const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
             ink[handleIndex] = newHandlePoint;
-            
             // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
             if (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
                 const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
                 oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
                 ink[oppositeHandleIndex] = oppositeHandlePoint;
             }
-
             return ink;
         })
 }
\ No newline at end of file
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 485376a34..1270a2dab 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -57,13 +57,10 @@ const strokeDataSchema = createSimpleSchema({
     "*": true
 });
 
-// Holistic class representing the store of an ink.
 @Deserializable("ink")
 export class InkField extends ObjectField {
     @serializable(list(object(strokeDataSchema)))
     readonly inkData: InkData;
-    // inkData: InkData;
-
 
     constructor(data: InkData) {
         super();
-- 
cgit v1.2.3-70-g09d2


From fabaad0251cf4f64c1af7a37a5f3d6622011c058 Mon Sep 17 00:00:00 2001
From: vkalev <vjk1883@gmail.com>
Date: Thu, 22 Jul 2021 11:30:18 -0400
Subject: adding broken indices to layoutDoc working

---
 src/client/views/InkControls.tsx        | 11 ++++++-----
 src/client/views/InkHandles.tsx         | 12 ++++++------
 src/client/views/InkStrokeProperties.ts | 11 ++++-------
 3 files changed, 16 insertions(+), 18 deletions(-)

(limited to 'src/client/views/InkControls.tsx')

diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index da7b0df16..23f22c774 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -16,7 +16,6 @@ export interface InkControlProps {
 
 @observer
 export class InkControls extends React.Component<InkControlProps> {
-    // @observable private _controlPoints: ControlPoint[] = [];
     @observable private _overControl = -1;
     @observable private _overAddPoint = -1;
 
@@ -38,10 +37,12 @@ export class InkControls extends React.Component<InkControlProps> {
                     InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
                     return false;
                 },
-                () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) =>
-                { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
-                    InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
-                }}));
+                () => controlUndo?.end(), 
+                emptyFunction);
+                // action((e: PointerEvent, doubleTap: boolean | undefined) =>
+                // { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
+                //     InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+                // }}));
         }
     }
 
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index ffb6986f1..28b6dd820 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -8,9 +8,10 @@ import { InkData, HandlePoint, HandleLine } from "../../fields/InkField";
 import { Transform } from "../util/Transform";
 import { Doc } from "../../fields/Doc";
 import { listSpec } from "../../fields/Schema";
+import { List } from "../../fields/List";
 import { Cast } from "../../fields/Types";
 
-export interface InkControlProps {
+export interface InkHandlesProps {
     inkDoc: Doc;
     data: InkData;
     format: number[];
@@ -18,7 +19,7 @@ export interface InkControlProps {
 }
 
 @observer
-export class InkHandles extends React.Component<InkControlProps> {
+export class InkHandles extends React.Component<InkHandlesProps> {
     /**
      * Handles the movement of a selected handle point when the user clicks and drags.
      * @param handleNum The index of the currently selected handle point.
@@ -50,11 +51,10 @@ export class InkHandles extends React.Component<InkControlProps> {
         const doc: Doc = this.props.inkDoc;
         if (["Alt"].includes(e.key)) {
             if (doc) {
-                let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
-                if (brokenIndices && brokenIndices.includes(controlIndex)) {
+                const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List;
+                if (brokenIndices && !brokenIndices.includes(controlIndex)) {
                     brokenIndices.push(controlIndex);
                 }
-
                 doc.brokenInkIndices = brokenIndices;
             }
         }
@@ -73,7 +73,7 @@ export class InkHandles extends React.Component<InkControlProps> {
                 handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 });
                 handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 });
             }
-            // adding first and last (single) handle lines
+            // Adding first and last (single) handle lines.
             handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
             handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
             for (let i = 2; i < data.length - 4; i += 4) {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 4c225210e..f218b4fbf 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -15,7 +15,6 @@ export class InkStrokeProperties {
     @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
-    // @observable _brokenIndices: List<number> = new List<number>();
 
     constructor() {
         InkStrokeProperties.Instance = this;
@@ -112,11 +111,9 @@ export class InkStrokeProperties {
                 }
                 
             }
-
             let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
-
             // Updating the indices of the control points whose handle tangency has been broken.
-            if(brokenIndices){
+            if (brokenIndices) {
                 brokenIndices = new List(brokenIndices.map((control) => { 
                     if (control >= newIndex) {
                         return control + 4; 
@@ -283,7 +280,7 @@ export class InkStrokeProperties {
     }
 
     /**
-     * Finds the angle between two inputted vectors.
+     * Finds the angle (in radians) between two inputted vectors.
      * 
      * α = arccos(a·b / |a|·|b|), where a and b are both vectors.
      */
@@ -323,9 +320,9 @@ export class InkStrokeProperties {
             const controlPoint = ink[controlIndex];
             const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
             ink[handleIndex] = newHandlePoint;
-            let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+            const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
             // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-            if (brokenIndices && !brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
+            if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
                 const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
                 oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
                 ink[oppositeHandleIndex] = oppositeHandlePoint;
-- 
cgit v1.2.3-70-g09d2

