From 2781f77d46612720865ac0c1d115d6e013806103 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Tue, 3 Dec 2019 17:34:58 -0500
Subject: improved multi-user touch stuff

---
 .../collectionFreeForm/CollectionFreeFormView.tsx           | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9506ce084..7267e94be 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -418,7 +418,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
         if (!e.cancelBubble) {
-            let pt = e.targetTouches.item(0);
+            let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+            let pt = myTouches[0];
             if (pt) {
                 if (InkingControl.Instance.selectedTool === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(e)) {
@@ -443,9 +444,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle2PointersMove = (e: TouchEvent) => {
         // pinch zooming
         if (!e.cancelBubble) {
-            let pt1: Touch | null = e.targetTouches.item(0);
-            let pt2: Touch | null = e.targetTouches.item(1);
-            if (!pt1 || !pt2) return;
+            let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+            let pt1 = myTouches[0];
+            let pt2 = myTouches[1];
 
             if (this.prevPoints.size === 2) {
                 let oldPoint1 = this.prevPoints.get(pt1.identifier);
@@ -923,8 +924,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const panx = -this.props.panX();
         const pany = -this.props.panY();
         const zoom = this.props.zoomScaling();
-        return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
-            {this.props.children()}
+        return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
+            {this.props.children}
         </div>;
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From cb2edb837ffa9df5ad966d44ac0c26622d6579f0 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Tue, 3 Dec 2019 17:57:37 -0500
Subject: first hint of recognizer?

---
 src/Utils.ts                                       |   3 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  13 +-
 src/ndollar.ts                                     | 148 +++++++++++----------
 3 files changed, 93 insertions(+), 71 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 37b509370..dc62d96e0 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,9 +3,12 @@ import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
 import { RouteStore } from './server/RouteStore';
+import { NDollarRecognizer } from './ndollar';
 
 export namespace Utils {
 
+    export const GestureRecognizer = new NDollarRecognizer(false);
+
     export const DRAG_THRESHOLD = 4;
 
     export function GenerateGuid(): string {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7267e94be..dcda13b00 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -337,9 +337,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (this._points.length > 1) {
             let B = this.svgBounds;
             let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
-            let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
-            this.addDocument(inkDoc);
-            this._points = [];
+
+            let result = Utils.GestureRecognizer.Recognize(new Array(points));
+            if (result) {
+                console.log(result.Name);
+            }
+            else {
+                let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
+                this.addDocument(inkDoc);
+                this._points = [];
+            }
         }
 
         document.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/ndollar.ts b/src/ndollar.ts
index 2d3903cc3..6d7b8c5d9 100644
--- a/src/ndollar.ts
+++ b/src/ndollar.ts
@@ -130,6 +130,10 @@ export class Multistroke {
     }
 }
 
+export enum Gestures {
+    Box = "box"
+}
+
 //
 // Result class
 //
@@ -140,7 +144,7 @@ export class Result {
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 16;
+const NumMultistrokes = 1;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -165,78 +169,86 @@ export class NDollarRecognizer {
         // one predefined multistroke for each multistroke type
         //
         this.Multistrokes = new Array(NumMultistrokes);
-        this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array(
-            new Array(new Point(30, 7), new Point(103, 7)),
-            new Array(new Point(66, 7), new Point(66, 87))
-        ));
-        this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array(
-            new Array(new Point(177, 92), new Point(177, 2)),
-            new Array(new Point(182, 1), new Point(246, 95)),
-            new Array(new Point(247, 87), new Point(247, 1))
-        ));
-        this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array(
-            new Array(new Point(345, 9), new Point(345, 87)),
-            new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87))
-        ));
-        this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array(
-            new Array(new Point(507, 8), new Point(507, 87)),
-            new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55))
-        ));
-        this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array(
-            new Array(new Point(30, 146), new Point(106, 222)),
-            new Array(new Point(30, 225), new Point(106, 146))
-        ));
-        this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array(
-            new Array(new Point(188, 137), new Point(188, 225)),
-            new Array(new Point(188, 180), new Point(241, 180)),
-            new Array(new Point(241, 137), new Point(241, 225))
-        ));
-        this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array(
-            new Array(new Point(371, 149), new Point(371, 221)),
-            new Array(new Point(341, 149), new Point(401, 149)),
-            new Array(new Point(341, 221), new Point(401, 221))
-        ));
-        this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array(
-            new Array(new Point(526, 142), new Point(526, 204)),
-            new Array(new Point(526, 221))
-        ));
-        this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array(
-            new Array(new Point(12, 347), new Point(119, 347))
-        ));
-        this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array(
-            new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397))
-        ));
-        this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array(
-            new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)),
-            new Array(new Point(418, 309), new Point(337, 390))
-        ));
-        this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array(
-            new Array(new Point(506, 349), new Point(574, 349)),
-            new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388))
-        ));
-        this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array(
-            new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)),
-            new Array(new Point(62, 464), new Point(62, 571))
-        ));
-        this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array(
-            new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)),
-            new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490))
-        ));
-        this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array(
-            new Array(new Point(325, 499), new Point(417, 557)),
-            new Array(new Point(417, 499), new Point(325, 557)),
-            new Array(new Point(371, 486), new Point(371, 571))
-        ));
-        this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array(
-            new Array(new Point(546, 465), new Point(546, 531)),
-            new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536))
+        this.Multistrokes[0] = new Multistroke(Gestures.Box, useBoundedRotationInvariance, new Array(
+            new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146))
         ));
+
+        //
+        // PREDEFINED STROKES
+        //
+
+        // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(30, 7), new Point(103, 7)),
+        //     new Array(new Point(66, 7), new Point(66, 87))
+        // ));
+        // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 92), new Point(177, 2)),
+        //     new Array(new Point(182, 1), new Point(246, 95)),
+        //     new Array(new Point(247, 87), new Point(247, 1))
+        // ));
+        // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(345, 9), new Point(345, 87)),
+        //     new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87))
+        // ));
+        // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(507, 8), new Point(507, 87)),
+        //     new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55))
+        // ));
+        // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(30, 146), new Point(106, 222)),
+        //     new Array(new Point(30, 225), new Point(106, 146))
+        // ));
+        // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(188, 137), new Point(188, 225)),
+        //     new Array(new Point(188, 180), new Point(241, 180)),
+        //     new Array(new Point(241, 137), new Point(241, 225))
+        // ));
+        // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(371, 149), new Point(371, 221)),
+        //     new Array(new Point(341, 149), new Point(401, 149)),
+        //     new Array(new Point(341, 221), new Point(401, 221))
+        // ));
+        // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(526, 142), new Point(526, 204)),
+        //     new Array(new Point(526, 221))
+        // ));
+        // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(12, 347), new Point(119, 347))
+        // ));
+        // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397))
+        // ));
+        // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)),
+        //     new Array(new Point(418, 309), new Point(337, 390))
+        // ));
+        // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(506, 349), new Point(574, 349)),
+        //     new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388))
+        // ));
+        // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)),
+        //     new Array(new Point(62, 464), new Point(62, 571))
+        // ));
+        // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)),
+        //     new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490))
+        // ));
+        // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(325, 499), new Point(417, 557)),
+        //     new Array(new Point(417, 499), new Point(325, 557)),
+        //     new Array(new Point(371, 486), new Point(371, 571))
+        // ));
+        // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(546, 465), new Point(546, 531)),
+        //     new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536))
+        // ));
         //
         // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures()
         //
     }
 
-    Recognize = (strokes: any[], useBoundedRotationInvariance: boolean, requireSameNoOfStrokes: boolean, useProtractor: boolean) => {
+    Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => {
         var t0 = Date.now();
         var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
         var candidate = new Unistroke("", useBoundedRotationInvariance, points);
@@ -267,7 +279,7 @@ export class NDollarRecognizer {
             }
         }
         var t1 = Date.now();
-        return (u == -1) ? new Result("No match.", 0.0, t1 - t0) : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
+        return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
     }
 
     AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
-- 
cgit v1.2.3-70-g09d2


From fb9f08295c10c35c11b647d00a3c830867dc3f7d Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 3 Dec 2019 18:54:41 -0500
Subject: nothing new, infrastructure stuffs

---
 src/Utils.ts                                       |   4 -
 src/client/documents/Documents.ts                  |   2 +-
 src/client/views/InkingStroke.tsx                  |   8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  22 +-
 src/ndollar.ts                                     | 531 ---------------------
 src/new_fields/InkField.ts                         |   6 +-
 src/pen-gestures/GestureUtils.ts                   |  35 ++
 src/pen-gestures/ndollar.ts                        | 529 ++++++++++++++++++++
 8 files changed, 584 insertions(+), 553 deletions(-)
 delete mode 100644 src/ndollar.ts
 create mode 100644 src/pen-gestures/GestureUtils.ts
 create mode 100644 src/pen-gestures/ndollar.ts

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index dc62d96e0..3db15f997 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,12 +3,8 @@ import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
 import { RouteStore } from './server/RouteStore';
-import { NDollarRecognizer } from './ndollar';
 
 export namespace Utils {
-
-    export const GestureRecognizer = new NDollarRecognizer(false);
-
     export const DRAG_THRESHOLD = 4;
 
     export function GenerateGuid(): string {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c5bf109a1..8c6aa2006 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -422,7 +422,7 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
         }
 
-        export function InkDocument(color: string, tool: number, strokeWidth: number, points: { x: number, y: number }[], options: DocumentOptions = {}) {
+        export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
             let doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options);
             doc.color = color;
             doc.strokeWidth = strokeWidth;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a27f106e3..a2e9f0e55 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -13,8 +13,8 @@ import React = require("react");
 type InkDocument = makeInterface<[typeof documentSchema]>;
 const InkDocument = makeInterface(documentSchema);
 
-export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) {
-    let pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, "");
+export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color?: string, width?: number) {
+    let pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
     return (
         <polyline
             points={pts}
@@ -36,8 +36,8 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
 
     render() {
         let data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
-        let xs = data.map(p => p.x);
-        let ys = data.map(p => p.y);
+        let xs = data.map(p => p.X);
+        let ys = data.map(p => p.Y);
         let left = Math.min(...xs);
         let top = Math.min(...ys);
         let right = Math.max(...xs);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index dcda13b00..6dd472c84 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -41,6 +41,7 @@ import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import { computedFn, keepAlive } from "mobx-utils";
 import { TraceMobx } from "../../../../new_fields/util";
+import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -272,7 +273,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return clusterColor;
     }
 
-    @observable private _points: { x: number, y: number }[] = [];
+    @observable private _points: { X: number, Y: number }[] = [];
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
@@ -288,7 +289,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 e.stopPropagation();
                 e.preventDefault();
                 let point = this.getTransform().transformPoint(e.pageX, e.pageY);
-                this._points.push({ x: point[0], y: point[1] });
+                this._points.push({ X: point[0], Y: point[1] });
             }
             // if not using a pen and in no ink mode
             else if (InkingControl.Instance.selectedTool === InkTool.None) {
@@ -336,11 +337,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         if (this._points.length > 1) {
             let B = this.svgBounds;
-            let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
+            let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
 
-            let result = Utils.GestureRecognizer.Recognize(new Array(points));
+            let result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
             if (result) {
                 console.log(result.Name);
+                this._points = [];
             }
             else {
                 let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
@@ -405,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             const selectedTool = InkingControl.Instance.selectedTool;
             if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
                 let point = this.getTransform().transformPoint(e.clientX, e.clientY);
-                this._points.push({ x: point[0], y: point[1] });
+                this._points.push({ X: point[0], Y: point[1] });
             }
             else if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(e)) {
@@ -440,7 +442,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 }
                 else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
                     let point = this.getTransform().transformPoint(pt.clientX, pt.clientY);
-                    this._points.push({ x: point[0], y: point[1] });
+                    this._points.push({ X: point[0], Y: point[1] });
                 }
             }
             e.stopPropagation();
@@ -846,8 +848,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @computed get svgBounds() {
-        let xs = this._points.map(p => p.x);
-        let ys = this._points.map(p => p.y);
+        let xs = this._points.map(p => p.X);
+        let ys = this._points.map(p => p.Y);
         let right = Math.max(...xs);
         let left = Math.min(...xs);
         let bottom = Math.max(...ys);
@@ -863,7 +865,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let B = this.svgBounds;
 
         return (
-            <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}>
+            <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, position: "relative", zIndex: 30000 }}>
                 {CreatePolyline(this._points, B.left, B.top)}
             </svg>
         );
@@ -932,7 +934,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         const pany = -this.props.panY();
         const zoom = this.props.zoomScaling();
         return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
-            {this.props.children}
+            {this.props.children()}
         </div>;
     }
 }
\ No newline at end of file
diff --git a/src/ndollar.ts b/src/ndollar.ts
deleted file mode 100644
index 6d7b8c5d9..000000000
--- a/src/ndollar.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-/**
- * The $N Multistroke Recognizer (JavaScript version)
- * Converted to TypeScript -syip2
- *
- *  Lisa Anthony, Ph.D.
- *  UMBC
- *  Information Systems Department
- *  1000 Hilltop Circle
- *  Baltimore, MD 21250
- *  lanthony@umbc.edu
- *
- *  Jacob O. Wobbrock, Ph.D.
- *  The Information School
- *  University of Washington
- *  Seattle, WA 98195-2840
- *  wobbrock@uw.edu
- *
- * The academic publications for the $N recognizer, and what should be
- * used to cite it, are:
- *
- *     Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke
- *     recognizer for user interface prototypes. Proceedings of Graphics
- *     Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto,
- *     Ontario: Canadian Information Processing Society, pp. 245-252.
- *     https://dl.acm.org/citation.cfm?id=1839258
- *
- *     Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and
- *     accurate multistroke recognizer. Proceedings of Graphics Interface
- *     (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario:
- *     Canadian Information Processing Society, pp. 117-120.
- *     https://dl.acm.org/citation.cfm?id=2305296
- *
- * The Protractor enhancement was separately published by Yang Li and programmed
- * here by Jacob O. Wobbrock and Lisa Anthony:
- *
- *     Li, Y. (2010). Protractor: A fast and accurate gesture
- *     recognizer. Proceedings of the ACM Conference on Human
- *     Factors in Computing Systems (CHI '10). Atlanta, Georgia
- *     (April 10-15, 2010). New York: ACM Press, pp. 2169-2172.
- *     https://dl.acm.org/citation.cfm?id=1753654
- *
- * This software is distributed under the "New BSD License" agreement:
- *
- * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony.
- * All rights reserved. Last updated July 14, 2018.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *    * Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *    * Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *    * Neither the names of UMBC nor the University of Washington,
- *      nor the names of its contributors may be used to endorse or promote
- *      products derived from this software without specific prior written
- *      permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
- * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
- * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock
- * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
- * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
-**/
-
-//
-// Point class
-//
-export class Point {
-    constructor(public X: number, public Y: number) { }
-}
-
-//
-// Rectangle class
-//
-export class Rectangle {
-    constructor(public X: number, public Y: number, public Width: number, public Height: number) { }
-}
-
-//
-// Unistroke class: a unistroke template
-//
-export class Unistroke {
-    public Points: Point[];
-    public StartUnitVector: Point;
-    public Vector: number[];
-
-    constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) {
-        this.Points = Resample(points, NumPoints);
-        var radians = IndicativeAngle(this.Points);
-        this.Points = RotateBy(this.Points, -radians);
-        this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold);
-        if (useBoundedRotationInvariance) {
-            this.Points = RotateBy(this.Points, +radians); // restore
-        }
-        this.Points = TranslateTo(this.Points, Origin);
-        this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex);
-        this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor
-    }
-}
-//
-// Multistroke class: a container for unistrokes
-//
-export class Multistroke {
-    public NumStrokes: number;
-    public Unistrokes: Unistroke[];
-
-    constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor
-    {
-        this.NumStrokes = strokes.length; // number of individual strokes
-
-        var order = new Array(strokes.length); // array of integer indices
-        for (var i = 0; i < strokes.length; i++) {
-            order[i] = i; // initialize
-        }
-        var orders = new Array(); // array of integer arrays
-        HeapPermute(strokes.length, order, /*out*/ orders);
-
-        var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays
-        this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke
-        for (var j = 0; j < unistrokes.length; j++) {
-            this.Unistrokes[j] = new Unistroke(name, useBoundedRotationInvariance, unistrokes[j]);
-        }
-    }
-}
-
-export enum Gestures {
-    Box = "box"
-}
-
-//
-// Result class
-//
-export class Result {
-    constructor(public Name: string, public Score: any, public Time: any) { }
-}
-
-//
-// NDollarRecognizer constants
-//
-const NumMultistrokes = 1;
-const NumPoints = 96;
-const SquareSize = 250.0;
-const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
-const Origin = new Point(0, 0);
-const Diagonal = Math.sqrt(SquareSize * SquareSize + SquareSize * SquareSize);
-const HalfDiagonal = 0.5 * Diagonal;
-const AngleRange = Deg2Rad(45.0);
-const AnglePrecision = Deg2Rad(2.0);
-const Phi = 0.5 * (-1.0 + Math.sqrt(5.0)); // Golden Ratio
-const StartAngleIndex = (NumPoints / 8); // eighth of gesture length
-const AngleSimilarityThreshold = Deg2Rad(30.0);
-
-//
-// NDollarRecognizer class
-//
-export class NDollarRecognizer {
-    public Multistrokes: Multistroke[];
-
-    constructor(useBoundedRotationInvariance: boolean) // constructor
-    {
-        //
-        // one predefined multistroke for each multistroke type
-        //
-        this.Multistrokes = new Array(NumMultistrokes);
-        this.Multistrokes[0] = new Multistroke(Gestures.Box, useBoundedRotationInvariance, new Array(
-            new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146))
-        ));
-
-        //
-        // PREDEFINED STROKES
-        //
-
-        // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(30, 7), new Point(103, 7)),
-        //     new Array(new Point(66, 7), new Point(66, 87))
-        // ));
-        // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(177, 92), new Point(177, 2)),
-        //     new Array(new Point(182, 1), new Point(246, 95)),
-        //     new Array(new Point(247, 87), new Point(247, 1))
-        // ));
-        // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(345, 9), new Point(345, 87)),
-        //     new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87))
-        // ));
-        // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(507, 8), new Point(507, 87)),
-        //     new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55))
-        // ));
-        // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(30, 146), new Point(106, 222)),
-        //     new Array(new Point(30, 225), new Point(106, 146))
-        // ));
-        // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(188, 137), new Point(188, 225)),
-        //     new Array(new Point(188, 180), new Point(241, 180)),
-        //     new Array(new Point(241, 137), new Point(241, 225))
-        // ));
-        // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(371, 149), new Point(371, 221)),
-        //     new Array(new Point(341, 149), new Point(401, 149)),
-        //     new Array(new Point(341, 221), new Point(401, 221))
-        // ));
-        // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(526, 142), new Point(526, 204)),
-        //     new Array(new Point(526, 221))
-        // ));
-        // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(12, 347), new Point(119, 347))
-        // ));
-        // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397))
-        // ));
-        // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)),
-        //     new Array(new Point(418, 309), new Point(337, 390))
-        // ));
-        // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(506, 349), new Point(574, 349)),
-        //     new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388))
-        // ));
-        // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)),
-        //     new Array(new Point(62, 464), new Point(62, 571))
-        // ));
-        // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)),
-        //     new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490))
-        // ));
-        // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(325, 499), new Point(417, 557)),
-        //     new Array(new Point(417, 499), new Point(325, 557)),
-        //     new Array(new Point(371, 486), new Point(371, 571))
-        // ));
-        // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(546, 465), new Point(546, 531)),
-        //     new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536))
-        // ));
-        //
-        // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures()
-        //
-    }
-
-    Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => {
-        var t0 = Date.now();
-        var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
-        var candidate = new Unistroke("", useBoundedRotationInvariance, points);
-
-        var u = -1;
-        var b = +Infinity;
-        for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template
-        {
-            if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
-            {
-                for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke
-                {
-                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
-                    {
-                        var d;
-                        if (useProtractor) {
-                            d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor
-                        }
-                        else {
-                            d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
-                        }
-                        if (d < b) {
-                            b = d; // best (least) distance
-                            u = i; // multistroke owner of unistroke
-                        }
-                    }
-                }
-            }
-        }
-        var t1 = Date.now();
-        return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
-    }
-
-    AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
-        this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
-        var num = 0;
-        for (var i = 0; i < this.Multistrokes.length; i++) {
-            if (this.Multistrokes[i].Name == name) {
-                num++;
-            }
-        }
-        return num;
-    }
-
-    DeleteUserGestures = () => {
-        this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set
-        return NumMultistrokes;
-    }
-}
-
-
-//
-// Private helper functions from here on down
-//
-function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
-    if (n == 1) {
-        orders[orders.length] = order.slice(); // append copy
-    } else {
-        for (var i = 0; i < n; i++) {
-            HeapPermute(n - 1, order, orders);
-            if (n % 2 == 1) { // swap 0, n-1
-                var tmp = order[0];
-                order[0] = order[n - 1];
-                order[n - 1] = tmp;
-            } else { // swap i, n-1
-                var tmp = order[i];
-                order[i] = order[n - 1];
-                order[n - 1] = tmp;
-            }
-        }
-    }
-}
-
-function MakeUnistrokes(strokes: any, orders: any) {
-    var unistrokes = new Array(); // array of point arrays
-    for (var r = 0; r < orders.length; r++) {
-        for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions
-        {
-            var unistroke = new Array(); // array of points
-            for (var i = 0; i < orders[r].length; i++) {
-                var pts;
-                if (((b >> i) & 1) == 1) {// is b's bit at index i on?
-                    pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse
-                }
-                else {
-                    pts = strokes[orders[r][i]].slice(); // copy
-                }
-                for (var p = 0; p < pts.length; p++) {
-                    unistroke[unistroke.length] = pts[p]; // append points
-                }
-            }
-            unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
-        }
-    }
-    return unistrokes;
-}
-
-function CombineStrokes(strokes: any) {
-    var points = new Array();
-    for (var s = 0; s < strokes.length; s++) {
-        for (var p = 0; p < strokes[s].length; p++)
-            points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y);
-    }
-    return points;
-}
-function Resample(points: any, n: any) {
-    var I = PathLength(points) / (n - 1); // interval length
-    var D = 0.0;
-    var newpoints = new Array(points[0]);
-    for (var i = 1; i < points.length; i++) {
-        var d = Distance(points[i - 1], points[i]);
-        if ((D + d) >= I) {
-            var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
-            var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y);
-            var q = new Point(qx, qy);
-            newpoints[newpoints.length] = q; // append new point 'q'
-            points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i
-            D = 0.0;
-        }
-        else D += d;
-    }
-    if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so
-        newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y);
-    return newpoints;
-}
-function IndicativeAngle(points: any) {
-    var c = Centroid(points);
-    return Math.atan2(c.Y - points[0].Y, c.X - points[0].X);
-}
-function RotateBy(points: any, radians: any) // rotates points around centroid
-{
-    var c = Centroid(points);
-    var cos = Math.cos(radians);
-    var sin = Math.sin(radians);
-    var newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X
-        var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y;
-        newpoints[newpoints.length] = new Point(qx, qy);
-    }
-    return newpoints;
-}
-function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D
-{
-    var B = BoundingBox(points);
-    var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
-    var newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width);
-        var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height);
-        newpoints[newpoints.length] = new Point(qx, qy);
-    }
-    return newpoints;
-}
-function TranslateTo(points: any, pt: any) // translates points' centroid
-{
-    var c = Centroid(points);
-    var newpoints = new Array();
-    for (var i = 0; i < points.length; i++) {
-        var qx = points[i].X + pt.X - c.X;
-        var qy = points[i].Y + pt.Y - c.Y;
-        newpoints[newpoints.length] = new Point(qx, qy);
-    }
-    return newpoints;
-}
-function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor
-{
-    var cos = 1.0;
-    var sin = 0.0;
-    if (useBoundedRotationInvariance) {
-        var iAngle = Math.atan2(points[0].Y, points[0].X);
-        var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0));
-        cos = Math.cos(baseOrientation - iAngle);
-        sin = Math.sin(baseOrientation - iAngle);
-    }
-    var sum = 0.0;
-    var vector = new Array<number>();
-    for (var i = 0; i < points.length; i++) {
-        var newX = points[i].X * cos - points[i].Y * sin;
-        var newY = points[i].Y * cos + points[i].X * sin;
-        vector[vector.length] = newX;
-        vector[vector.length] = newY;
-        sum += newX * newX + newY * newY;
-    }
-    var magnitude = Math.sqrt(sum);
-    for (var i = 0; i < vector.length; i++) {
-        vector[i] /= magnitude;
-    }
-    return vector;
-}
-function OptimalCosineDistance(v1: any, v2: any) // for Protractor
-{
-    var a = 0.0;
-    var b = 0.0;
-    for (var i = 0; i < v1.length; i += 2) {
-        a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1];
-        b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i];
-    }
-    var angle = Math.atan(b / a);
-    return Math.acos(a * Math.cos(angle) + b * Math.sin(angle));
-}
-function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) {
-    var x1 = Phi * a + (1.0 - Phi) * b;
-    var f1 = DistanceAtAngle(points, T, x1);
-    var x2 = (1.0 - Phi) * a + Phi * b;
-    var f2 = DistanceAtAngle(points, T, x2);
-    while (Math.abs(b - a) > threshold) {
-        if (f1 < f2) {
-            b = x2;
-            x2 = x1;
-            f2 = f1;
-            x1 = Phi * a + (1.0 - Phi) * b;
-            f1 = DistanceAtAngle(points, T, x1);
-        } else {
-            a = x1;
-            x1 = x2;
-            f1 = f2;
-            x2 = (1.0 - Phi) * a + Phi * b;
-            f2 = DistanceAtAngle(points, T, x2);
-        }
-    }
-    return Math.min(f1, f2);
-}
-function DistanceAtAngle(points: any, T: any, radians: any) {
-    var newpoints = RotateBy(points, radians);
-    return PathDistance(newpoints, T.Points);
-}
-function Centroid(points: any) {
-    var x = 0.0, y = 0.0;
-    for (var i = 0; i < points.length; i++) {
-        x += points[i].X;
-        y += points[i].Y;
-    }
-    x /= points.length;
-    y /= points.length;
-    return new Point(x, y);
-}
-function BoundingBox(points: any) {
-    var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
-    for (var i = 0; i < points.length; i++) {
-        minX = Math.min(minX, points[i].X);
-        minY = Math.min(minY, points[i].Y);
-        maxX = Math.max(maxX, points[i].X);
-        maxY = Math.max(maxY, points[i].Y);
-    }
-    return new Rectangle(minX, minY, maxX - minX, maxY - minY);
-}
-function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths
-{
-    var d = 0.0;
-    for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length
-        d += Distance(pts1[i], pts2[i]);
-    return d / pts1.length;
-}
-function PathLength(points: any) // length traversed by a point path
-{
-    var d = 0.0;
-    for (var i = 1; i < points.length; i++)
-        d += Distance(points[i - 1], points[i]);
-    return d;
-}
-function Distance(p1: any, p2: any) // distance between two points
-{
-    var dx = p2.X - p1.X;
-    var dy = p2.Y - p1.Y;
-    return Math.sqrt(dx * dx + dy * dy);
-}
-function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector
-{
-    var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
-    var len = Math.sqrt(v.X * v.X + v.Y * v.Y);
-    return new Point(v.X / len, v.Y / len);
-}
-function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2
-{
-    var n = (v1.X * v2.X + v1.Y * v2.Y);
-    var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1]
-    return Math.acos(c); // arc cosine of the vector dot product
-}
-function Deg2Rad(d: any) { return (d * Math.PI / 180.0); }
\ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 2d8bb582a..83d631958 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -13,14 +13,14 @@ export enum InkTool {
 }
 
 export interface PointData {
-    x: number;
-    y: number;
+    X: number;
+    Y: number;
 }
 
 export type InkData = Array<PointData>;
 
 const pointSchema = createSimpleSchema({
-    x: true, y: true
+    X: true, Y: true
 });
 
 const strokeDataSchema = createSimpleSchema({
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
new file mode 100644
index 000000000..caaa0736d
--- /dev/null
+++ b/src/pen-gestures/GestureUtils.ts
@@ -0,0 +1,35 @@
+import { NDollarRecognizer } from "./ndollar";
+import { Type } from "typescript";
+import { InkField } from "../new_fields/InkField";
+import { Docs } from "../client/documents/Documents";
+import { Doc } from "../new_fields/Doc";
+
+export namespace GestureUtils {
+    namespace GestureDataTypes {
+        export type BoxData = Doc[];
+    }
+
+    export const GestureRecognizer = new NDollarRecognizer(false);
+
+    export enum Gestures {
+        Box = "box"
+    }
+
+    export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] {
+        switch (name) {
+            case Gestures.Box:
+                if (gestureData as GestureDataTypes.BoxData) {
+                    return BoxOptions(gestureData as GestureDataTypes.BoxData);
+                }
+                break;
+        }
+        throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts");
+    }
+
+    function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] {
+        if (gestureData instanceof Doc[]) {
+            return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})];
+        }
+        return [];
+    }
+}
\ No newline at end of file
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
new file mode 100644
index 000000000..5d4384202
--- /dev/null
+++ b/src/pen-gestures/ndollar.ts
@@ -0,0 +1,529 @@
+import { GestureUtils } from "./GestureUtils";
+
+/**
+ * The $N Multistroke Recognizer (JavaScript version)
+ * Converted to TypeScript -syip2
+ *
+ *  Lisa Anthony, Ph.D.
+ *  UMBC
+ *  Information Systems Department
+ *  1000 Hilltop Circle
+ *  Baltimore, MD 21250
+ *  lanthony@umbc.edu
+ *
+ *  Jacob O. Wobbrock, Ph.D.
+ *  The Information School
+ *  University of Washington
+ *  Seattle, WA 98195-2840
+ *  wobbrock@uw.edu
+ *
+ * The academic publications for the $N recognizer, and what should be
+ * used to cite it, are:
+ *
+ *     Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke
+ *     recognizer for user interface prototypes. Proceedings of Graphics
+ *     Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto,
+ *     Ontario: Canadian Information Processing Society, pp. 245-252.
+ *     https://dl.acm.org/citation.cfm?id=1839258
+ *
+ *     Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and
+ *     accurate multistroke recognizer. Proceedings of Graphics Interface
+ *     (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario:
+ *     Canadian Information Processing Society, pp. 117-120.
+ *     https://dl.acm.org/citation.cfm?id=2305296
+ *
+ * The Protractor enhancement was separately published by Yang Li and programmed
+ * here by Jacob O. Wobbrock and Lisa Anthony:
+ *
+ *     Li, Y. (2010). Protractor: A fast and accurate gesture
+ *     recognizer. Proceedings of the ACM Conference on Human
+ *     Factors in Computing Systems (CHI '10). Atlanta, Georgia
+ *     (April 10-15, 2010). New York: ACM Press, pp. 2169-2172.
+ *     https://dl.acm.org/citation.cfm?id=1753654
+ *
+ * This software is distributed under the "New BSD License" agreement:
+ *
+ * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony.
+ * All rights reserved. Last updated July 14, 2018.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *    * Neither the names of UMBC nor the University of Washington,
+ *      nor the names of its contributors may be used to endorse or promote
+ *      products derived from this software without specific prior written
+ *      permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+**/
+
+//
+// Point class
+//
+export class Point {
+    constructor(public X: number, public Y: number) { }
+}
+
+//
+// Rectangle class
+//
+export class Rectangle {
+    constructor(public X: number, public Y: number, public Width: number, public Height: number) { }
+}
+
+//
+// Unistroke class: a unistroke template
+//
+export class Unistroke {
+    public Points: Point[];
+    public StartUnitVector: Point;
+    public Vector: number[];
+
+    constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) {
+        this.Points = Resample(points, NumPoints);
+        var radians = IndicativeAngle(this.Points);
+        this.Points = RotateBy(this.Points, -radians);
+        this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold);
+        if (useBoundedRotationInvariance) {
+            this.Points = RotateBy(this.Points, +radians); // restore
+        }
+        this.Points = TranslateTo(this.Points, Origin);
+        this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex);
+        this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor
+    }
+}
+//
+// Multistroke class: a container for unistrokes
+//
+export class Multistroke {
+    public NumStrokes: number;
+    public Unistrokes: Unistroke[];
+
+    constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor
+    {
+        this.NumStrokes = strokes.length; // number of individual strokes
+
+        var order = new Array(strokes.length); // array of integer indices
+        for (var i = 0; i < strokes.length; i++) {
+            order[i] = i; // initialize
+        }
+        var orders = new Array(); // array of integer arrays
+        HeapPermute(strokes.length, order, /*out*/ orders);
+
+        var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays
+        this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke
+        for (var j = 0; j < unistrokes.length; j++) {
+            this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]);
+        }
+    }
+}
+
+//
+// Result class
+//
+export class Result {
+    constructor(public Name: string, public Score: any, public Time: any) { }
+}
+
+//
+// NDollarRecognizer constants
+//
+const NumMultistrokes = 1;
+const NumPoints = 96;
+const SquareSize = 250.0;
+const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
+const Origin = new Point(0, 0);
+const Diagonal = Math.sqrt(SquareSize * SquareSize + SquareSize * SquareSize);
+const HalfDiagonal = 0.5 * Diagonal;
+const AngleRange = Deg2Rad(45.0);
+const AnglePrecision = Deg2Rad(2.0);
+const Phi = 0.5 * (-1.0 + Math.sqrt(5.0)); // Golden Ratio
+const StartAngleIndex = (NumPoints / 8); // eighth of gesture length
+const AngleSimilarityThreshold = Deg2Rad(30.0);
+
+//
+// NDollarRecognizer class
+//
+export class NDollarRecognizer {
+    public Multistrokes: Multistroke[];
+
+    constructor(useBoundedRotationInvariance: boolean) // constructor
+    {
+        //
+        // one predefined multistroke for each multistroke type
+        //
+        this.Multistrokes = new Array(NumMultistrokes);
+        this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array(
+            new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146))
+        ));
+
+        //
+        // PREDEFINED STROKES
+        //
+
+        // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(30, 7), new Point(103, 7)),
+        //     new Array(new Point(66, 7), new Point(66, 87))
+        // ));
+        // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 92), new Point(177, 2)),
+        //     new Array(new Point(182, 1), new Point(246, 95)),
+        //     new Array(new Point(247, 87), new Point(247, 1))
+        // ));
+        // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(345, 9), new Point(345, 87)),
+        //     new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87))
+        // ));
+        // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(507, 8), new Point(507, 87)),
+        //     new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55))
+        // ));
+        // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(30, 146), new Point(106, 222)),
+        //     new Array(new Point(30, 225), new Point(106, 146))
+        // ));
+        // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(188, 137), new Point(188, 225)),
+        //     new Array(new Point(188, 180), new Point(241, 180)),
+        //     new Array(new Point(241, 137), new Point(241, 225))
+        // ));
+        // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(371, 149), new Point(371, 221)),
+        //     new Array(new Point(341, 149), new Point(401, 149)),
+        //     new Array(new Point(341, 221), new Point(401, 221))
+        // ));
+        // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(526, 142), new Point(526, 204)),
+        //     new Array(new Point(526, 221))
+        // ));
+        // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(12, 347), new Point(119, 347))
+        // ));
+        // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397))
+        // ));
+        // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)),
+        //     new Array(new Point(418, 309), new Point(337, 390))
+        // ));
+        // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(506, 349), new Point(574, 349)),
+        //     new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388))
+        // ));
+        // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)),
+        //     new Array(new Point(62, 464), new Point(62, 571))
+        // ));
+        // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)),
+        //     new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490))
+        // ));
+        // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(325, 499), new Point(417, 557)),
+        //     new Array(new Point(417, 499), new Point(325, 557)),
+        //     new Array(new Point(371, 486), new Point(371, 571))
+        // ));
+        // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array(
+        //     new Array(new Point(546, 465), new Point(546, 531)),
+        //     new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536))
+        // ));
+        //
+        // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures()
+        //
+    }
+
+    Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => {
+        var t0 = Date.now();
+        var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
+        var candidate = new Unistroke("", useBoundedRotationInvariance, points);
+
+        var u = -1;
+        var b = +Infinity;
+        for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template
+        {
+            if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
+            {
+                for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke
+                {
+                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
+                    {
+                        var d;
+                        if (useProtractor) {
+                            d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor
+                        }
+                        else {
+                            d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
+                        }
+                        if (d < b) {
+                            b = d; // best (least) distance
+                            u = i; // multistroke owner of unistroke
+                        }
+                    }
+                }
+            }
+        }
+        var t1 = Date.now();
+        return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
+    }
+
+    AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
+        this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
+        var num = 0;
+        for (var i = 0; i < this.Multistrokes.length; i++) {
+            if (this.Multistrokes[i].Name == name) {
+                num++;
+            }
+        }
+        return num;
+    }
+
+    DeleteUserGestures = () => {
+        this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set
+        return NumMultistrokes;
+    }
+}
+
+
+//
+// Private helper functions from here on down
+//
+function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
+    if (n == 1) {
+        orders[orders.length] = order.slice(); // append copy
+    } else {
+        for (var i = 0; i < n; i++) {
+            HeapPermute(n - 1, order, orders);
+            if (n % 2 == 1) { // swap 0, n-1
+                var tmp = order[0];
+                order[0] = order[n - 1];
+                order[n - 1] = tmp;
+            } else { // swap i, n-1
+                var tmp = order[i];
+                order[i] = order[n - 1];
+                order[n - 1] = tmp;
+            }
+        }
+    }
+}
+
+function MakeUnistrokes(strokes: any, orders: any) {
+    var unistrokes = new Array(); // array of point arrays
+    for (var r = 0; r < orders.length; r++) {
+        for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions
+        {
+            var unistroke = new Array(); // array of points
+            for (var i = 0; i < orders[r].length; i++) {
+                var pts;
+                if (((b >> i) & 1) == 1) {// is b's bit at index i on?
+                    pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse
+                }
+                else {
+                    pts = strokes[orders[r][i]].slice(); // copy
+                }
+                for (var p = 0; p < pts.length; p++) {
+                    unistroke[unistroke.length] = pts[p]; // append points
+                }
+            }
+            unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
+        }
+    }
+    return unistrokes;
+}
+
+function CombineStrokes(strokes: any) {
+    var points = new Array();
+    for (var s = 0; s < strokes.length; s++) {
+        for (var p = 0; p < strokes[s].length; p++)
+            points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y);
+    }
+    return points;
+}
+function Resample(points: any, n: any) {
+    var I = PathLength(points) / (n - 1); // interval length
+    var D = 0.0;
+    var newpoints = new Array(points[0]);
+    for (var i = 1; i < points.length; i++) {
+        var d = Distance(points[i - 1], points[i]);
+        if ((D + d) >= I) {
+            var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
+            var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y);
+            var q = new Point(qx, qy);
+            newpoints[newpoints.length] = q; // append new point 'q'
+            points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i
+            D = 0.0;
+        }
+        else D += d;
+    }
+    if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so
+        newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y);
+    return newpoints;
+}
+function IndicativeAngle(points: any) {
+    var c = Centroid(points);
+    return Math.atan2(c.Y - points[0].Y, c.X - points[0].X);
+}
+function RotateBy(points: any, radians: any) // rotates points around centroid
+{
+    var c = Centroid(points);
+    var cos = Math.cos(radians);
+    var sin = Math.sin(radians);
+    var newpoints = new Array();
+    for (var i = 0; i < points.length; i++) {
+        var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X
+        var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y;
+        newpoints[newpoints.length] = new Point(qx, qy);
+    }
+    return newpoints;
+}
+function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D
+{
+    var B = BoundingBox(points);
+    var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
+    var newpoints = new Array();
+    for (var i = 0; i < points.length; i++) {
+        var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width);
+        var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height);
+        newpoints[newpoints.length] = new Point(qx, qy);
+    }
+    return newpoints;
+}
+function TranslateTo(points: any, pt: any) // translates points' centroid
+{
+    var c = Centroid(points);
+    var newpoints = new Array();
+    for (var i = 0; i < points.length; i++) {
+        var qx = points[i].X + pt.X - c.X;
+        var qy = points[i].Y + pt.Y - c.Y;
+        newpoints[newpoints.length] = new Point(qx, qy);
+    }
+    return newpoints;
+}
+function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor
+{
+    var cos = 1.0;
+    var sin = 0.0;
+    if (useBoundedRotationInvariance) {
+        var iAngle = Math.atan2(points[0].Y, points[0].X);
+        var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0));
+        cos = Math.cos(baseOrientation - iAngle);
+        sin = Math.sin(baseOrientation - iAngle);
+    }
+    var sum = 0.0;
+    var vector = new Array<number>();
+    for (var i = 0; i < points.length; i++) {
+        var newX = points[i].X * cos - points[i].Y * sin;
+        var newY = points[i].Y * cos + points[i].X * sin;
+        vector[vector.length] = newX;
+        vector[vector.length] = newY;
+        sum += newX * newX + newY * newY;
+    }
+    var magnitude = Math.sqrt(sum);
+    for (var i = 0; i < vector.length; i++) {
+        vector[i] /= magnitude;
+    }
+    return vector;
+}
+function OptimalCosineDistance(v1: any, v2: any) // for Protractor
+{
+    var a = 0.0;
+    var b = 0.0;
+    for (var i = 0; i < v1.length; i += 2) {
+        a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1];
+        b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i];
+    }
+    var angle = Math.atan(b / a);
+    return Math.acos(a * Math.cos(angle) + b * Math.sin(angle));
+}
+function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) {
+    var x1 = Phi * a + (1.0 - Phi) * b;
+    var f1 = DistanceAtAngle(points, T, x1);
+    var x2 = (1.0 - Phi) * a + Phi * b;
+    var f2 = DistanceAtAngle(points, T, x2);
+    while (Math.abs(b - a) > threshold) {
+        if (f1 < f2) {
+            b = x2;
+            x2 = x1;
+            f2 = f1;
+            x1 = Phi * a + (1.0 - Phi) * b;
+            f1 = DistanceAtAngle(points, T, x1);
+        } else {
+            a = x1;
+            x1 = x2;
+            f1 = f2;
+            x2 = (1.0 - Phi) * a + Phi * b;
+            f2 = DistanceAtAngle(points, T, x2);
+        }
+    }
+    return Math.min(f1, f2);
+}
+function DistanceAtAngle(points: any, T: any, radians: any) {
+    var newpoints = RotateBy(points, radians);
+    return PathDistance(newpoints, T.Points);
+}
+function Centroid(points: any) {
+    var x = 0.0, y = 0.0;
+    for (var i = 0; i < points.length; i++) {
+        x += points[i].X;
+        y += points[i].Y;
+    }
+    x /= points.length;
+    y /= points.length;
+    return new Point(x, y);
+}
+function BoundingBox(points: any) {
+    var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
+    for (var i = 0; i < points.length; i++) {
+        minX = Math.min(minX, points[i].X);
+        minY = Math.min(minY, points[i].Y);
+        maxX = Math.max(maxX, points[i].X);
+        maxY = Math.max(maxY, points[i].Y);
+    }
+    return new Rectangle(minX, minY, maxX - minX, maxY - minY);
+}
+function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths
+{
+    var d = 0.0;
+    for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length
+        d += Distance(pts1[i], pts2[i]);
+    return d / pts1.length;
+}
+function PathLength(points: any) // length traversed by a point path
+{
+    var d = 0.0;
+    for (var i = 1; i < points.length; i++)
+        d += Distance(points[i - 1], points[i]);
+    return d;
+}
+function Distance(p1: any, p2: any) // distance between two points
+{
+    var dx = p2.X - p1.X;
+    var dy = p2.Y - p1.Y;
+    return Math.sqrt(dx * dx + dy * dy);
+}
+function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector
+{
+    var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
+    var len = Math.sqrt(v.X * v.X + v.Y * v.Y);
+    return new Point(v.X / len, v.Y / len);
+}
+function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2
+{
+    var n = (v1.X * v2.X + v1.Y * v2.Y);
+    var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1]
+    return Math.acos(c); // arc cosine of the vector dot product
+}
+function Deg2Rad(d: any) { return (d * Math.PI / 180.0); }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 766e47cd144be6f0bcbb5787dbd174aa1bba3690 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Sat, 7 Dec 2019 16:53:43 -0500
Subject: box !!!!

---
 src/client/util/InteractionUtils.ts                |  2 +-
 src/client/views/Touchable.tsx                     |  4 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 23 ++++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            |  8 ++++----
 src/pen-gestures/GestureUtils.ts                   | 16 ++++-----------
 src/pen-gestures/ndollar.ts                        |  2 +-
 6 files changed, 31 insertions(+), 24 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index 7390890c6..7c386abc2 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -53,7 +53,7 @@ export namespace InteractionUtils {
      * @param oldPoint2 - previous point 2
      */
     export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        let threshold = 3;
+        let threshold = window.devicePixelRatio;
         let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
         let newDist = TwoPointEuclidist(pt1, pt2);
 
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 5dcfec768..a647dec04 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -18,9 +18,9 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     protected onTouchStart = (e: React.TouchEvent): void => {
         for (let i = 0; i < e.targetTouches.length; i++) {
             let pt: any = e.targetTouches.item(i);
-            // pen is also a touch, but with a radius of 0.5 (at least with the surface pens). i doubt anyone's fingers are 2 pixels wide,
+            // pen is also a touch, but with a radius of 0.5 (at least with the surface pens).
             // and this seems to be the only way of differentiating pen and touch on touch events
-            if (pt.radiusX > 2 && pt.radiusY > 2) {
+            if (pt.radiusX > 0.5 && pt.radiusY > 0.5) {
                 this.prevPoints.set(pt.identifier, pt);
             }
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6dd472c84..bec0ace5a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -340,8 +340,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
 
             let result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
-            if (result) {
-                console.log(result.Name);
+            if (result && result.Score > 0.75) {
+                switch (result.Name) {
+                    case GestureUtils.Gestures.Box:
+                        let bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.y)), b: Math.max(...this._points.map(p => p.Y)) };
+                        let sel = this.getActiveDocuments().filter(doc => {
+                            let l = NumCast(doc.x);
+                            let r = l + doc[WidthSym]();
+                            let t = NumCast(doc.y);
+                            let b = t + doc[HeightSym]();
+                            doc.x = l - B.left - B.width / 2;
+                            doc.y = t - B.top - B.height / 2;
+                            return !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
+                        });
+                        this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 }));
+                        sel.forEach(d => this.props.removeDocument(d));
+                        break;
+                }
                 this._points = [];
             }
             else {
@@ -474,8 +489,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         let rawDelta = (dir * (d1 + d2));
 
                         // this floors and ceils the delta value to prevent jitteriness
-                        let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16);
-                        this.zoom(centerX, centerY, delta);
+                        let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8);
+                        this.zoom(centerX, centerY, delta * window.devicePixelRatio);
                         this.prevPoints.set(pt1.identifier, pt1);
                         this.prevPoints.set(pt2.identifier, pt2);
                     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 38c46b258..dfeadb8c0 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -142,7 +142,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
             e.stopPropagation();
             let preventDefault = true;
-            if (this._doubleTap && this.props.renderDepth && !this.onClickHandler ?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
+            if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 let fullScreenAlias = Doc.MakeAlias(this.props.Document);
                 if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
                     fullScreenAlias.layoutKey = "layoutCustom";
@@ -362,7 +362,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     setCustomView = (custom: boolean): void => {
-        if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) {
+        if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
             Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
         } else {
             custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
@@ -687,10 +687,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont}
             onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
             onPointerEnter={e => {
-                console.log("Brush" + this.props.Document.title);
+                // console.log("Brush" + this.props.Document.title);
                 Doc.BrushDoc(this.props.Document);
             }} onPointerLeave={e => {
-                console.log("UnBrush" + this.props.Document.title);
+                // console.log("UnBrush"     + this.props.Document.title);
                 Doc.UnBrushDoc(this.props.Document);
 
             }}
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 7a14452c8..955dad5b4 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -2,7 +2,9 @@ import { NDollarRecognizer } from "./ndollar";
 import { Type } from "typescript";
 import { InkField } from "../new_fields/InkField";
 import { Docs } from "../client/documents/Documents";
-import { Doc } from "../new_fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../new_fields/Doc";
+import { NumCast } from "../new_fields/Types";
+import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView";
 
 export namespace GestureUtils {
     namespace GestureDataTypes {
@@ -15,21 +17,11 @@ export namespace GestureUtils {
 
     export const GestureRecognizer = new NDollarRecognizer(false);
 
-    export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] {
+    export function GestureOptions(name: string, gestureData?: any): (params: {}) => any {
         switch (name) {
             case Gestures.Box:
-                if (gestureData as GestureDataTypes.BoxData) {
-                    return BoxOptions(gestureData as GestureDataTypes.BoxData);
-                }
                 break;
         }
         throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts");
     }
-
-    function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] {
-        if (gestureData instanceof Array) {
-            return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})];
-        }
-        return [];
-    }
 }
\ No newline at end of file
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 5d4384202..8c8e079a4 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -168,7 +168,7 @@ export class NDollarRecognizer {
         //
         this.Multistrokes = new Array(NumMultistrokes);
         this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array(
-            new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146))
+            new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146))
         ));
 
         //
-- 
cgit v1.2.3-70-g09d2


From 3a2aaf5ffec2a568bb749c6ac0df8367a150af39 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sun, 8 Dec 2019 14:46:46 -0500
Subject: links!

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 40 +++++++++++++++++++---
 src/pen-gestures/GestureUtils.ts                   |  3 +-
 src/pen-gestures/ndollar.ts                        |  8 ++---
 3 files changed, 41 insertions(+), 10 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bec0ace5a..5a7644d05 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -13,7 +13,7 @@ import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fiel
 import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
-import { Docs } from "../../../documents/Documents";
+import { Docs, DocUtils } from "../../../documents/Documents";
 import { DocumentType } from "../../../documents/DocumentTypes";
 import { DocumentManager } from "../../../util/DocumentManager";
 import { DragManager } from "../../../util/DragManager";
@@ -42,6 +42,7 @@ import React = require("react");
 import { computedFn, keepAlive } from "mobx-utils";
 import { TraceMobx } from "../../../../new_fields/util";
 import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
+import { LinkManager } from "../../../util/LinkManager";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -340,7 +341,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
 
             let result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
-            if (result && result.Score > 0.75) {
+            let actionPerformed = false;
+            if (result && result.Score > 0.7) {
                 switch (result.Name) {
                     case GestureUtils.Gestures.Box:
                         let bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.y)), b: Math.max(...this._points.map(p => p.Y)) };
@@ -355,11 +357,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         });
                         this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 }));
                         sel.forEach(d => this.props.removeDocument(d));
+                        actionPerformed = true;
+                        break;
+                    case GestureUtils.Gestures.Line:
+                        let ep1 = this._points[0];
+                        let ep2 = this._points[this._points.length - 1];
+                        let d1: Doc | undefined;
+                        let d2: Doc | undefined;
+                        this.getActiveDocuments().map(doc => {
+                            let l = NumCast(doc.x);
+                            let r = l + doc[WidthSym]();
+                            let t = NumCast(doc.y);
+                            let b = t + doc[HeightSym]();
+                            if (!d1 && l < ep1.X && r > ep1.X && t < ep1.Y && b > ep1.Y) {
+                                d1 = doc;
+                            }
+                            else if (!d2 && l < ep2.X && r > ep2.X && t < ep2.Y && b > ep2.Y) {
+                                d2 = doc;
+                            }
+                        });
+                        if (d1 && d2) {
+                            if (!LinkManager.Instance.doesLinkExist(d1, d2)) {
+                                DocUtils.MakeLink({ doc: d1 }, { doc: d2 });
+                                actionPerformed = true;
+                            }
+                        }
                         break;
                 }
-                this._points = [];
+                if (actionPerformed) {
+                    this._points = [];
+                }
             }
-            else {
+
+            if (!actionPerformed) {
                 let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
                 this.addDocument(inkDoc);
                 this._points = [];
@@ -880,7 +910,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let B = this.svgBounds;
 
         return (
-            <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, position: "relative", zIndex: 30000 }}>
+            <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, position: "absolute", zIndex: 30000 }}>
                 {CreatePolyline(this._points, B.left, B.top)}
             </svg>
         );
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 955dad5b4..59a85b66b 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -12,7 +12,8 @@ export namespace GestureUtils {
     }
 
     export enum Gestures {
-        Box = "box"
+        Box = "box",
+        Line = "line"
     }
 
     export const GestureRecognizer = new NDollarRecognizer(false);
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 8c8e079a4..12c2b25bb 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -142,7 +142,7 @@ export class Result {
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 1;
+const NumMultistrokes = 2;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -170,6 +170,9 @@ export class NDollarRecognizer {
         this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array(
             new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146))
         ));
+        this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(
+            new Array(new Point(12, 347), new Point(119, 347))
+        ));
 
         //
         // PREDEFINED STROKES
@@ -210,9 +213,6 @@ export class NDollarRecognizer {
         //     new Array(new Point(526, 142), new Point(526, 204)),
         //     new Array(new Point(526, 221))
         // ));
-        // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array(
-        //     new Array(new Point(12, 347), new Point(119, 347))
-        // ));
         // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array(
         //     new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397))
         // ));
-- 
cgit v1.2.3-70-g09d2


From 4ead7eccb8ed9dd5708384315f8c5454b9725106 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Sun, 8 Dec 2019 15:07:52 -0500
Subject: smal fix

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx    | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5a7644d05..7b507f739 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -351,9 +351,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                             let r = l + doc[WidthSym]();
                             let t = NumCast(doc.y);
                             let b = t + doc[HeightSym]();
-                            doc.x = l - B.left - B.width / 2;
-                            doc.y = t - B.top - B.height / 2;
-                            return !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
+                            let pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
+                            if (pass) {
+                                doc.x = l - B.left - B.width / 2;
+                                doc.y = t - B.top - B.height / 2;
+                            }
+                            return pass;
                         });
                         this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 }));
                         sel.forEach(d => this.props.removeDocument(d));
-- 
cgit v1.2.3-70-g09d2


From d16636a8246dadb486476fe98da63fc0db4d36dc Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 7 Jan 2020 14:49:27 -0500
Subject: resizing works, the math is just wrong looool

---
 src/client/util/InteractionUtils.ts                |   2 +-
 src/client/views/Touchable.tsx                     |  21 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  44 ++++-
 src/client/views/nodes/DocumentView.tsx            | 191 +++++++++++++++++----
 src/client/views/nodes/FormattedTextBox.scss       |  20 ++-
 5 files changed, 220 insertions(+), 58 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index 7c386abc2..2d3671041 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -8,7 +8,7 @@ export namespace InteractionUtils {
     const REACT_POINTER_PEN_BUTTON = 0;
     const ERASER_BUTTON = 5;
 
-    export function GetMyTargetTouches(e: TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] {
+    export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] {
         let myTouches = new Array<React.Touch>();
         for (let i = 0; i < e.targetTouches.length; i++) {
             let pt = e.targetTouches.item(i);
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index a647dec04..7b0581376 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -26,7 +26,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         }
 
         if (this.prevPoints.size) {
-            console.log(e.targetTouches.length);
             switch (this.prevPoints.size) {
                 case 1:
                     this.handle1PointerDown(e);
@@ -35,11 +34,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
                     this.handle2PointersDown(e);
                     break;
             }
-
-            document.removeEventListener("touchmove", this.onTouch);
-            document.addEventListener("touchmove", this.onTouch);
-            document.removeEventListener("touchend", this.onTouchEnd);
-            document.addEventListener("touchend", this.onTouchEnd);
         }
     }
 
@@ -110,6 +104,17 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         e.preventDefault();
     }
 
-    handle1PointerDown = (e: React.TouchEvent): any => { };
-    handle2PointersDown = (e: React.TouchEvent): any => { };
+    handle1PointerDown = (e: React.TouchEvent): any => {
+        document.removeEventListener("touchmove", this.onTouch);
+        document.addEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+        document.addEventListener("touchend", this.onTouchEnd);
+    }
+
+    handle2PointersDown = (e: React.TouchEvent): any => {
+        document.removeEventListener("touchmove", this.onTouch);
+        document.addEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchend", this.onTouchEnd);
+        document.addEventListener("touchend", this.onTouchEnd);
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7b507f739..75690ab2c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -326,9 +326,30 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     handle1PointerDown = (e: React.TouchEvent) => {
+        if (e.nativeEvent.cancelBubble) return;
         let pt = e.targetTouches.item(0);
         if (pt) {
             this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
+            if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
+                document.removeEventListener("touchmove", this.onTouch);
+                document.addEventListener("touchmove", this.onTouch);
+                document.removeEventListener("touchend", this.onTouchEnd);
+                document.addEventListener("touchend", this.onTouchEnd);
+                if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                    e.stopPropagation();
+                    e.preventDefault();
+                    let point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
+                    this._points.push({ X: point[0], Y: point[1] });
+                }
+                else if (InkingControl.Instance.selectedTool === InkTool.None) {
+                    this._lastX = pt.pageX;
+                    this._lastY = pt.pageY;
+                }
+                else {
+                    e.stopPropagation();
+                    e.preventDefault();
+                }
+            }
         }
     }
 
@@ -544,14 +565,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     handle2PointersDown = (e: React.TouchEvent) => {
-        let pt1: React.Touch | null = e.targetTouches.item(0);
-        let pt2: React.Touch | null = e.targetTouches.item(1);
-        if (!pt1 || !pt2) return;
-
-        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-        this._lastX = centerX;
-        this._lastY = centerY;
+        if (!e.nativeEvent.cancelBubble && this.props.active(true)) {
+            let pt1: React.Touch | null = e.targetTouches.item(0);
+            let pt2: React.Touch | null = e.targetTouches.item(1);
+            if (!pt1 || !pt2) return;
+
+            let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+            let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+            this._lastX = centerX;
+            this._lastY = centerY;
+            document.removeEventListener("touchmove", this.onTouch);
+            document.addEventListener("touchmove", this.onTouch);
+            document.removeEventListener("touchend", this.onTouchEnd);
+            document.addEventListener("touchend", this.onTouchEnd);
+            e.stopPropagation();
+        }
     }
 
     cleanUpInteractions = () => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dfeadb8c0..1780d9789 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { action, computed, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { Document } from '../../../new_fields/documentSchemas';
+import { Document, PositionDocument } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
@@ -193,28 +193,165 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
+    handle1PointerDown = (e: React.TouchEvent) => {
+        if (!e.nativeEvent.cancelBubble) {
+            let touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0];
+            this._downX = touch.clientX;
+            this._downY = touch.clientY;
+            this._hitTemplateDrag = false;
+            for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
+                if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
+                    this._hitTemplateDrag = true;
+                }
+            }
+            if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation();
+            document.removeEventListener("touchmove", this.onTouch);
+            document.addEventListener("touchmove", this.onTouch);
+            document.removeEventListener("touchend", this.onTouchEnd);
+            document.addEventListener("touchend", this.onTouchEnd);
+            if ((e.nativeEvent as any).formattedHandled) e.stopPropagation();
+            console.log("down")
+        }
+    }
+
+    handle1PointerMove = (e: TouchEvent) => {
+        if ((e as any).formattedHandled) { e.stopPropagation; return; }
+        if (e.cancelBubble && this.active) {
+            document.removeEventListener("touchmove", this.onTouch);
+        }
+        else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
+            let touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0];
+            if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
+                if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
+                    document.removeEventListener("touchmove", this.onTouch);
+                    document.removeEventListener("touchend", this.onTouchEnd);
+                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+                }
+            }
+            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
+            e.preventDefault();
+
+        }
+    }
+
+    handle2PointersDown = (e: React.TouchEvent) => {
+        if (!e.nativeEvent.cancelBubble && !this.isSelected()) {
+            e.stopPropagation();
+            e.preventDefault();
+
+            document.removeEventListener("touchmove", this.onTouch);
+            document.addEventListener("touchmove", this.onTouch);
+            document.removeEventListener("touchend", this.onTouchEnd);
+            document.addEventListener("touchend", this.onTouchEnd);
+        }
+    }
+
+    @action
+    handle2PointersMove = (e: TouchEvent) => {
+        let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+        let pt1 = myTouches[0];
+        let pt2 = myTouches[1];
+        let oldPoint1 = this.prevPoints.get(pt1.identifier);
+        let oldPoint2 = this.prevPoints.get(pt2.identifier);
+        let pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
+        if (pinching !== 0 && oldPoint1 && oldPoint2) {
+            // let dX = (Math.min(pt1.clientX, pt2.clientX) - Math.min(oldPoint1.clientX, oldPoint2.clientX));
+            // let dY = (Math.min(pt1.clientY, pt2.clientY) - Math.min(oldPoint1.clientY, oldPoint2.clientY));
+            // let dX = Math.sign(Math.abs(pt1.clientX - oldPoint1.clientX) - Math.abs(pt2.clientX - oldPoint2.clientX));
+            // let dY = Math.sign(Math.abs(pt1.clientY - oldPoint1.clientY) - Math.abs(pt2.clientY - oldPoint2.clientY));
+            // let dW = -dX;
+            // let dH = -dY;
+            let dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX));
+            let dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY));
+            let dX = -1 * Math.sign(dW);
+            let dY = -1 * Math.sign(dH);
+
+            if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
+                let doc = PositionDocument(this.props.Document);
+                let layoutDoc = PositionDocument(Doc.Layout(this.props.Document));
+                let nwidth = layoutDoc.nativeWidth || 0;
+                let nheight = layoutDoc.nativeHeight || 0;
+                let width = (layoutDoc.width || 0);
+                let height = (layoutDoc.height || (nheight / nwidth * width));
+                let scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling();
+                let actualdW = Math.max(width + (dW * scale), 20);
+                let actualdH = Math.max(height + (dH * scale), 20);
+                doc.x = (doc.x || 0) + dX * (actualdW - width);
+                doc.y = (doc.y || 0) + dY * (actualdH - height);
+                let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
+                if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
+                    layoutDoc.ignoreAspect = false;
+                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                }
+                if (fixedAspect && (!nwidth || !nheight)) {
+                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                }
+                if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
+                    if (Math.abs(dW) > Math.abs(dH)) {
+                        if (!fixedAspect) {
+                            layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
+                        }
+                        layoutDoc.width = actualdW;
+                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
+                        else layoutDoc.height = actualdH;
+                    }
+                    else {
+                        if (!fixedAspect) {
+                            layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
+                        }
+                        layoutDoc.height = actualdH;
+                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
+                        else layoutDoc.width = actualdW;
+                    }
+                } else {
+                    dW && (layoutDoc.width = actualdW);
+                    dH && (layoutDoc.height = actualdH);
+                    dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
+                }
+            }
+            // let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX))
+            // this.props.Document.width = newWidth;
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
+
     onPointerDown = (e: React.PointerEvent): void => {
-        if ((e.nativeEvent.cancelBubble && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)))
-            // return if we're inking, and not selecting a button document
-            || (InkingControl.Instance.selectedTool !== InkTool.None && !this.Document.onClick)
-            // return if using pen or eraser
-            || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) return;
-        this._downX = e.clientX;
-        this._downY = e.clientY;
-        this._hitTemplateDrag = false;
-        // this whole section needs to move somewhere else.  We're trying to initiate a special "template" drag where
-        // this document is the template and we apply it to whatever we drop it on.
-        for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
-            if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
-                this._hitTemplateDrag = true;
+        // console.log(e.button)
+        // console.log(e.nativeEvent)
+        // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
+        if (!InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE)) {
+            e.stopPropagation();
+            return;
+        }
+        if ((!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart)) {
+            // if ((e.nativeEvent.cancelBubble && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)))
+            //     // return if we're inking, and not selecting a button document
+            //     || (InkingControl.Instance.selectedTool !== InkTool.None && !this.Document.onClick)
+            //     // return if using pen or eraser
+            //     || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) {
+            //     return;
+            // }
+
+            this._downX = e.clientX;
+            this._downY = e.clientY;
+            this._hitTemplateDrag = false;
+            // this whole section needs to move somewhere else.  We're trying to initiate a special "template" drag where
+            // this document is the template and we apply it to whatever we drop it on.
+            for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
+                if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
+                    this._hitTemplateDrag = true;
+                }
             }
+            if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active.  if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            document.addEventListener("pointermove", this.onPointerMove);
+            document.addEventListener("pointerup", this.onPointerUp);
+            if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
         }
-        if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active.  if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        document.addEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointerup", this.onPointerUp);
-        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
     }
 
     onPointerMove = (e: PointerEvent): void => {
@@ -649,20 +786,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
     }
 
-    @action
-    handle2PointersMove = (e: TouchEvent) => {
-        let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
-        let pt1 = myTouches[0];
-        let pt2 = myTouches[1];
-        let oldPoint1 = this.prevPoints.get(pt1.identifier);
-        let oldPoint2 = this.prevPoints.get(pt2.identifier);
-        let pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
-        if (pinching !== 0) {
-            let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX))
-            this.props.Document.width = newWidth;
-        }
-    }
-
     render() {
         if (!this.props.Document) return (null);
         const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 77cdd3d42..2e5848db4 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -11,6 +11,7 @@
 }
 
 .formattedTextBox-cont {
+    touch-action: none;
     cursor: text;
     background: inherit;
     padding: 0;
@@ -50,6 +51,7 @@
     width: 100%;
     height: 100%;
 }
+
 .formattedTextBox-sidebar-handle {
     position: absolute;
     top: calc(50% - 17.5px);
@@ -58,19 +60,23 @@
     background: lightgray;
     border-radius: 20px;
 }
-.formattedTextBox-cont > .formattedTextBox-sidebar-handle {
+
+.formattedTextBox-cont>.formattedTextBox-sidebar-handle {
     right: 0;
     left: unset;
 }
-.formattedTextBox-sidebar, .formattedTextBox-sidebar-inking {
-    border-left: dashed 1px black; 
-    height: 100%; 
+
+.formattedTextBox-sidebar,
+.formattedTextBox-sidebar-inking {
+    border-left: dashed 1px black;
+    height: 100%;
     display: inline-block;
     position: absolute;
     right: 0;
-    > .formattedTextBox-sidebar-handle {
-        right:unset;
-        left:-5;
+
+    >.formattedTextBox-sidebar-handle {
+        right: unset;
+        left: -5;
     }
 }
 
-- 
cgit v1.2.3-70-g09d2

