From cef6852d597ce67637466afb36c3498dc84211f6 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Wed, 8 Jan 2020 19:48:28 -0500
Subject: gestures are now overlayed and can span collections/panes!

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 171 +++++++--------------
 1 file changed, 56 insertions(+), 115 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 eb5a074bb..9656debf3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -272,11 +272,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return clusterColor;
     }
 
-    @observable private _points: { X: number, Y: number }[] = [];
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble) return;
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+            return;
+        }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
         if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
             document.removeEventListener("pointermove", this.onPointerMove);
@@ -284,14 +285,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             document.addEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointerup", this.onPointerUp);
             // if physically using a pen or we're in pen or highlighter mode
-            if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
-                e.stopPropagation();
-                e.preventDefault();
-                const point = this.getTransform().transformPoint(e.pageX, e.pageY);
-                this._points.push({ X: point[0], Y: point[1] });
-            }
+            // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+            //     e.stopPropagation();
+            //     e.preventDefault();
+            //     const point = this.getTransform().transformPoint(e.pageX, e.pageY);
+            //     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) {
+            if (InkingControl.Instance.selectedTool === InkTool.None) {
                 this._lastX = e.pageX;
                 this._lastY = e.pageY;
             }
@@ -332,13 +333,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 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();
-                    const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
-                    this._points.push({ X: point[0], Y: point[1] });
-                }
-                else if (InkingControl.Instance.selectedTool === InkTool.None) {
+                // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                //     e.stopPropagation();
+                //     e.preventDefault();
+                //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
+                //     this._points.push({ X: point[0], Y: point[1] });
+                // }
+                if (InkingControl.Instance.selectedTool === InkTool.None) {
                     this._lastX = pt.pageX;
                     this._lastY = pt.pageY;
                     e.stopPropagation();
@@ -352,72 +353,44 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && this._points.length <= 1) return;
-
-        if (this._points.length > 1) {
-            const B = this.svgBounds;
-            const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
-
-            const result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
-            let actionPerformed = false;
-            if (result && result.Score > 0.7) {
-                switch (result.Name) {
-                    case GestureUtils.Gestures.Box:
-                        const 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)) };
-                        const sel = this.getActiveDocuments().filter(doc => {
-                            const l = NumCast(doc.x);
-                            const r = l + doc[WidthSym]();
-                            const t = NumCast(doc.y);
-                            const b = t + doc[HeightSym]();
-                            const 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));
-                        actionPerformed = true;
-                        break;
-                    case GestureUtils.Gestures.Line:
-                        const ep1 = this._points[0];
-                        const ep2 = this._points[this._points.length - 1];
-                        let d1: Doc | undefined;
-                        let d2: Doc | undefined;
-                        this.getActiveDocuments().map(doc => {
-                            const l = NumCast(doc.x);
-                            const r = l + doc[WidthSym]();
-                            const t = NumCast(doc.y);
-                            const 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;
-                }
-                if (actionPerformed) {
-                    this._points = [];
-                }
-            }
-
-            if (!actionPerformed) {
-                const 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 });
+    @undoBatch
+    onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
+        switch (ge.gesture) {
+            case GestureUtils.Gestures.Stroke:
+                const points = ge.points;
+                const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
+                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height });
                 this.addDocument(inkDoc);
-                this._points = [];
-            }
+                e.stopPropagation();
+                break;
+            case GestureUtils.Gestures.Box:
+                const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
+                const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
+                const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
+                const bWidth = bounds.r - bounds.x;
+                const bHeight = bounds.b - bounds.y;
+                const sel = this.getActiveDocuments().filter(doc => {
+                    const l = NumCast(doc.x);
+                    const r = l + doc[WidthSym]();
+                    const t = NumCast(doc.y);
+                    const b = t + doc[HeightSym]();
+                    const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
+                    if (pass) {
+                        doc.x = l - bounds.x - bWidth / 2;
+                        doc.y = t - bounds.y - bHeight / 2;
+                    }
+                    return pass;
+                });
+                this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 }));
+                sel.forEach(d => this.props.removeDocument(d));
+                break;
+
         }
+    }
+
+    @action
+    onPointerUp = (e: PointerEvent): void => {
+        if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return;
 
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
@@ -473,11 +446,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
         if (!e.cancelBubble) {
             const selectedTool = InkingControl.Instance.selectedTool;
-            if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
-                const point = this.getTransform().transformPoint(e.clientX, e.clientY);
-                this._points.push({ X: point[0], Y: point[1] });
-            }
-            else if (selectedTool === InkTool.None) {
+            if (selectedTool === InkTool.None) {
                 if (this._hitCluster && this.tryDragCluster(e)) {
                     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();
@@ -508,10 +477,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     }
                     this.pan(pt);
                 }
-                else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-                    const point = this.getTransform().transformPoint(pt.clientX, pt.clientY);
-                    this._points.push({ X: point[0], Y: point[1] });
-                }
             }
             e.stopPropagation();
             e.preventDefault();
@@ -942,34 +907,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         ];
     }
 
-    @computed get svgBounds() {
-        const xs = this._points.map(p => p.X);
-        const ys = this._points.map(p => p.Y);
-        const right = Math.max(...xs);
-        const left = Math.min(...xs);
-        const bottom = Math.max(...ys);
-        const top = Math.min(...ys);
-        return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
-    }
-
-    @computed get currentStroke() {
-        if (this._points.length <= 1) {
-            return (null);
-        }
-
-        const B = this.svgBounds;
-
-        return (
-            <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>
-        );
-    }
-
     children = () => {
         const eles: JSX.Element[] = [];
         this.extensionDoc && (eles.push(...this.childViews()));
-        this.currentStroke && (eles.push(this.currentStroke));
+        // this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
         return eles;
     }
@@ -998,7 +939,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
         if (!this.extensionDoc) return (null);
         // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
-        return <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+        return <div className={"collectionfreeformview-container"} ref={this.createDropAndGestureTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
             style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
             onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
             {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
-- 
cgit v1.2.3-70-g09d2


From 36eed6cc3a3387a1f7755a848aea4e19e2645e14 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Thu, 9 Jan 2020 19:55:52 -0500
Subject: some stuff that ive played around with today. mobile interface, five
 fingers, other stuff

---
 package-lock.json                                  | 27 +++++++++
 package.json                                       |  1 +
 src/client/views/GestureOverlay.scss               |  2 +-
 src/client/views/GestureOverlay.tsx                | 69 +++++++++++++---------
 src/client/views/InkingControl.tsx                 | 21 ++++---
 src/client/views/MainView.tsx                      |  4 +-
 src/client/views/Palette.scss                      | 18 ++++++
 src/client/views/Palette.tsx                       | 25 ++++++++
 src/client/views/Touchable.tsx                     | 18 ++++++
 .../views/collections/CollectionSchemaView.scss    | 14 +++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 54 +++++++++++++++++
 src/client/views/nodes/DocumentView.tsx            | 17 +-----
 src/mobile/ImageUpload.tsx                         | 31 +++++++---
 src/mobile/MobileInterface.tsx                     | 69 ++++++++++++++++++++++
 src/new_fields/InkField.ts                         |  3 +-
 src/pen-gestures/GestureUtils.ts                   |  3 +-
 .../authentication/models/current_user_utils.ts    | 26 ++++++++
 17 files changed, 335 insertions(+), 67 deletions(-)
 create mode 100644 src/client/views/Palette.scss
 create mode 100644 src/client/views/Palette.tsx
 create mode 100644 src/mobile/MobileInterface.tsx

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

diff --git a/package-lock.json b/package-lock.json
index d807d75a1..244b2e7e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2040,6 +2040,11 @@
             "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
             "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
         },
+        "avl": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/avl/-/avl-1.4.4.tgz",
+            "integrity": "sha512-vMjF4jhT2N0naO1fsBTA/lZ2AB70ECBv41na7oBe8B5PCgMVvNrWG+VRKMlbG5Mk5UGbMxzUD9fXK/IBsmmO5A=="
+        },
         "awesome-typescript-loader": {
             "version": "5.2.1",
             "resolved": "https://registry.npmjs.org/awesome-typescript-loader/-/awesome-typescript-loader-5.2.1.tgz",
@@ -2314,6 +2319,14 @@
                 "tweetnacl": "^0.14.3"
             }
         },
+        "bentley-ottman-sweepline": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/bentley-ottman-sweepline/-/bentley-ottman-sweepline-0.1.4.tgz",
+            "integrity": "sha512-Qptw9uoNLM0zLiJfA+9PqFrF5b6pMiJNyoWgqONq4/wVml4NC2N8S4RXYXn4xXR4JEBhHF+euVwwML7D9X1LqA==",
+            "requires": {
+                "avl": "^1.4.0"
+            }
+        },
         "better-assert": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
@@ -4316,6 +4329,14 @@
                 "minimalistic-crypto-utils": "^1.0.0"
             }
         },
+        "emit-logger": {
+            "version": "github:chocolateboy/emit-logger#b9d25a2d939e42f29c940861e9648bd0fb810070",
+            "from": "github:chocolateboy/emit-logger#better-emitter-name",
+            "requires": {
+                "chalk": "^1.1.1",
+                "moment": "^2.10.6"
+            }
+        },
         "emoji-regex": {
             "version": "7.0.3",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@@ -7065,6 +7086,7 @@
                 "babel-runtime": "^6.22.0",
                 "bluebird": "^3.4.7",
                 "boolify-string": "^2.0.2",
+                "emit-logger": "github:chocolateboy/emit-logger#better-emitter-name",
                 "lodash": "^4.17.4",
                 "semver": "^5.0.3",
                 "source-map-support": "^0.5.9"
@@ -8545,6 +8567,11 @@
                 }
             }
         },
+        "moment": {
+            "version": "2.24.0",
+            "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
+            "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
+        },
         "mongodb": {
             "version": "3.2.7",
             "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.2.7.tgz",
diff --git a/package.json b/package.json
index 9bd2273f8..2a72fac80 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
         "async": "^2.6.2",
         "babel-runtime": "^6.26.0",
         "bcrypt-nodejs": "0.0.3",
+        "bentley-ottman-sweepline": "^0.1.4",
         "bluebird": "^3.5.3",
         "body-parser": "^1.18.3",
         "bootstrap": "^4.3.1",
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index cbc1b6e7d..31601efd4 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -1,6 +1,6 @@
 .gestureOverlay-cont {
     width: 100vw;
-    height: 100vw;
+    height: 100vh;
     position: absolute;
     top: 0;
     left: 0;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index c88e3f7ae..848927912 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -43,9 +43,43 @@ export default class GestureOverlay extends Touchable {
 
     @action
     onPointerMove = (e: PointerEvent) => {
-        this._points.push({ X: e.clientX, Y: e.clientY });
-        e.stopPropagation();
-        e.preventDefault();
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+            this._points.push({ X: e.clientX, Y: e.clientY });
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
+
+    handleLineGesture = (): boolean => {
+        let actionPerformed = false;
+        const B = this.svgBounds;
+        const ep1 = this._points[0];
+        const ep2 = this._points[this._points.length - 1];
+
+        const target1 = document.elementFromPoint(ep1.X, ep1.Y);
+        const target2 = document.elementFromPoint(ep2.X, ep2.Y);
+        const callback = (doc: Doc) => {
+            if (!this._d1) {
+                this._d1 = doc;
+            }
+            else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
+                DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
+                actionPerformed = true;
+            }
+        }
+        const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+            {
+                bubbles: true,
+                detail: {
+                    points: this._points,
+                    gesture: GestureUtils.Gestures.Line,
+                    bounds: B,
+                    callbackFn: callback
+                }
+            });
+        target1?.dispatchEvent(ge);
+        target2?.dispatchEvent(ge);
+        return actionPerformed;
     }
 
     @action
@@ -72,31 +106,10 @@ export default class GestureOverlay extends Touchable {
                         actionPerformed = true;
                         break;
                     case GestureUtils.Gestures.Line:
-                        const ep1 = this._points[0];
-                        const ep2 = this._points[this._points.length - 1];
-                        const target1 = document.elementFromPoint(ep1.X, ep1.Y);
-                        const target2 = document.elementFromPoint(ep2.X, ep2.Y);
-                        const callback = (doc: Doc) => {
-                            if (!this._d1) {
-                                this._d1 = doc;
-                            }
-                            else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
-                                DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
-                                actionPerformed = true;
-                            }
-                        }
-                        const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
-                            {
-                                bubbles: true,
-                                detail: {
-                                    points: this._points,
-                                    gesture: GestureUtils.Gestures.Line,
-                                    bounds: B,
-                                    callbackFn: callback
-                                }
-                            })
-                        target1?.dispatchEvent(ge);
-                        target2?.dispatchEvent(ge);
+                        actionPerformed = this.handleLineGesture();
+                        break;
+                    case GestureUtils.Gestures.Scribble:
+                        console.log("scribble");
                         break;
                 }
                 if (actionPerformed) {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index e33f193b8..243123352 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -4,19 +4,18 @@ import { Doc } from "../../new_fields/Doc";
 import { InkTool } from "../../new_fields/InkField";
 import { List } from "../../new_fields/List";
 import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../new_fields/Types";
 import { Utils } from "../../Utils";
 import { Scripting } from "../util/Scripting";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 
-
 export class InkingControl {
     @observable static Instance: InkingControl;
-    @observable private _selectedTool: InkTool = InkTool.None;
-    @observable private _selectedColor: string = "rgb(244, 67, 54)";
-    @observable private _selectedWidth: string = "5";
+    @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(CurrentUserUtils.UserDocument.inkTool)) ?? InkTool.None; }
+    @computed private get _selectedColor(): string { return FieldValue(StrCast(CurrentUserUtils.UserDocument.inkColor)) ?? "rgb(244, 67, 54)"; }
+    @computed private get _selectedWidth(): string { return FieldValue(StrCast(CurrentUserUtils.UserDocument.inkWidth)) ?? "5"; }
     @observable public _open: boolean = false;
 
     constructor() {
@@ -24,7 +23,8 @@ export class InkingControl {
     }
 
     switchTool = action((tool: InkTool): void => {
-        this._selectedTool = tool;
+        // this._selectedTool = tool;
+        CurrentUserUtils.UserDocument.inkTool = tool;
     });
     decimalToHexString(number: number) {
         if (number < 0) {
@@ -36,7 +36,7 @@ export class InkingControl {
 
     @undoBatch
     switchColor = action((color: ColorState): void => {
-        this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+        CurrentUserUtils.UserDocument.inkColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
 
         if (InkingControl.Instance.selectedTool === InkTool.None) {
             const selected = SelectionManager.SelectedDocuments();
@@ -99,7 +99,8 @@ export class InkingControl {
     });
     @action
     switchWidth = (width: string): void => {
-        this._selectedWidth = width;
+        // this._selectedWidth = width;
+        CurrentUserUtils.UserDocument.inkWidth = width;
     }
 
     @computed
@@ -114,7 +115,8 @@ export class InkingControl {
 
     @action
     updateSelectedColor(value: string) {
-        this._selectedColor = value;
+        // this._selectedColor = value;
+        CurrentUserUtils.UserDocument.inkColor = value;
     }
 
     @computed
@@ -127,6 +129,7 @@ Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { Ink
 Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
 Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function activateScrubber(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Scrubber : InkTool.None); });
+Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
 Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 14c1803c0..f463ca306 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
     faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
-    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt
+    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -135,6 +135,8 @@ export class MainView extends React.Component {
         library.add(faChevronRight);
         library.add(faEllipsisV);
         library.add(faMusic);
+        library.add(faPhone);
+        library.add(faStamp);
         this.initEventListeners();
         this.initAuthenticationRouters();
     }
diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss
new file mode 100644
index 000000000..60004c81f
--- /dev/null
+++ b/src/client/views/Palette.scss
@@ -0,0 +1,18 @@
+.palette-container {
+    .palette-thumb {
+        width: 300px;
+        height: 300px;
+        touch-action: pan-x;
+        overflow: scroll;
+
+        .palette-thumbContent {
+            width: 100%;
+            height: 100%;
+
+            .palette-button {
+                width: 100px;
+                height: 100px;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
new file mode 100644
index 000000000..390b7e2c2
--- /dev/null
+++ b/src/client/views/Palette.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+import "./Palette.scss";
+import { PointData } from "../../new_fields/InkField";
+
+export interface PaletteProps {
+    x: number;
+    y: number;
+    thumb: number[];
+}
+
+export default class Palette extends React.Component<PaletteProps> {
+    render() {
+        return (
+            <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
+                <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}>
+                    <div className="palette-thumbContent">
+                        <div className="palette-button" style={{ background: "green" }} onPointerDown={() => console.log("hi")}>1</div>
+                        <div className="palette-button" style={{ background: "red" }}>2</div>
+                        <div className="palette-button" style={{ background: "blue" }}>3</div>
+                    </div>
+                </div>
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 251cd41e5..3eb66ff72 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { action } from 'mobx';
 import { InteractionUtils } from '../util/InteractionUtils';
+import { SelectionManager } from '../util/SelectionManager';
 
 const HOLD_DURATION = 1000;
 
@@ -34,11 +35,19 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
                 case 1:
                     this.handle1PointerDown(e);
                     e.persist();
+                    if (this.holdTimer) {
+                        clearTimeout(this.holdTimer)
+                        this.holdTimer = undefined;
+                    }
                     this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION);
+                    console.log(this.holdTimer);
                     break;
                 case 2:
                     this.handle2PointersDown(e);
                     break;
+                case 5:
+                    this.handleHandDown(e);
+                    break;
             }
         }
     }
@@ -54,7 +63,9 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
         this._touchDrag = true;
         if (this.holdTimer) {
+            console.log("clear");
             clearTimeout(this.holdTimer);
+            this.holdTimer = undefined;
         }
         switch (myTouches.length) {
             case 1:
@@ -88,7 +99,9 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             }
         }
         if (this.holdTimer) {
+            console.log("clear");
             clearTimeout(this.holdTimer);
+            this.holdTimer = undefined;
         }
         this._touchDrag = false;
         e.stopPropagation();
@@ -137,4 +150,9 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         e.stopPropagation();
         e.preventDefault();
     }
+
+    handleHandDown = (e: React.TouchEvent) => {
+        e.stopPropagation();
+        e.preventDefault();
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index cb95dcbbc..8b3d332af 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -15,6 +15,11 @@
     display: flex;
     justify-content: space-between;
     flex-wrap: nowrap;
+    touch-action: none;
+
+    div {
+        touch-action: none;
+    }
 
 
     .collectionSchemaView-tableContainer {
@@ -49,7 +54,7 @@
     .rt-table {
         height: 100%;
         display: -webkit-inline-box;
-        direction: ltr; 
+        direction: ltr;
         overflow: visible;
     }
 
@@ -202,7 +207,7 @@ button.add-column {
         border-bottom: 1px solid lightgray;
 
         &:first-child {
-            padding-top : 0;
+            padding-top: 0;
         }
 
         &:last-child {
@@ -231,7 +236,7 @@ button.add-column {
         transition: background-color 0.2s;
 
         &:hover {
-            background-color: $light-color-secondary; 
+            background-color: $light-color-secondary;
         }
 
         &.active {
@@ -267,7 +272,7 @@ button.add-column {
             overflow-y: scroll;
             position: absolute;
             top: 28px;
-            box-shadow: 0 10px 16px rgba(0,0,0,0.1);
+            box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1);
 
             .key-option {
                 background-color: $light-color;
@@ -380,6 +385,7 @@ button.add-column {
 
     &.editing {
         padding: 0;
+
         input {
             outline: 0;
             border: none;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9656debf3..d7cccc036 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -44,6 +44,7 @@ import { TraceMobx } from "../../../../new_fields/util";
 import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
 import { LinkManager } from "../../../util/LinkManager";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import Palette from "../../Palette";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -444,6 +445,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             return;
         }
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+            return;
+        }
         if (!e.cancelBubble) {
             const selectedTool = InkingControl.Instance.selectedTool;
             if (selectedTool === InkTool.None) {
@@ -844,6 +848,53 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData);
     }
 
+    private thumbIdentifier?: number;
+    private hand?: React.Touch[];
+
+    @action
+    handleHandDown = (e: React.TouchEvent) => {
+        const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+        this.hand = fingers;
+        const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+        this.thumbIdentifier = thumb?.identifier;
+        const others = fingers.filter(f => f !== thumb);
+        const minX = Math.min(...others.map(f => f.clientX));
+        const minY = Math.min(...others.map(f => f.clientY));
+        const t = this.getTransform().transformPoint(minX, minY);
+        const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+        this._palette = <Palette x={t[0]} y={t[1]} thumb={th} />;
+
+        document.removeEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchmove", this.handleHandMove);
+        document.addEventListener("touchmove", this.handleHandMove);
+        document.removeEventListener("touchend", this.handleHandUp);
+        document.addEventListener("touchend", this.handleHandUp);
+    }
+
+    @action
+    handleHandMove = (e: TouchEvent) => {
+        for (let i = 0; i < e.changedTouches.length; i++) {
+            const pt = e.changedTouches.item(i);
+            if (pt?.identifier === this.thumbIdentifier) {
+            }
+        }
+    }
+
+    @action
+    handleHandUp = (e: TouchEvent) => {
+        console.log(e.changedTouches.length);
+        this.onTouchEnd(e);
+        if (this.prevPoints.size < 3) {
+            if (this.hand) {
+                for (const h of this.hand) {
+                    this.prevPoints.has(h.identifier) && this.prevPoints.delete(h.identifier);
+                }
+            }
+            this._palette = undefined;
+            document.removeEventListener("touchend", this.handleHandUp);
+        }
+    }
+
     onContextMenu = (e: React.MouseEvent) => {
         const layoutItems: ContextMenuProps[] = [];
 
@@ -907,9 +958,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         ];
     }
 
+    @observable private _palette?: JSX.Element;
+
     children = () => {
         const eles: JSX.Element[] = [];
         this.extensionDoc && (eles.push(...this.childViews()));
+        this._palette && (eles.push(this._palette));
         // this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
         return eles;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7d6b8bf01..415d0e2ef 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -256,7 +256,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             document.removeEventListener("touchend", this.onTouchEnd);
             document.addEventListener("touchend", this.onTouchEnd);
             if ((e.nativeEvent as any).formattedHandled) e.stopPropagation();
-            console.log("down")
         }
     }
 
@@ -327,6 +326,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 const 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;
                 }
@@ -357,8 +357,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     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();
         }
@@ -374,19 +372,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             return;
         }
-        // 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;
@@ -408,6 +394,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     onPointerMove = (e: PointerEvent): void => {
         if ((e as any).formattedHandled) { e.stopPropagation(); return; }
+        if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return;
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index ec698b151..0b0280519 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,8 @@ import { List } from '../new_fields/List';
 import { observer } from 'mobx-react';
 import { observable } from 'mobx';
 import { Utils } from '../Utils';
+import MobileInterface from './MobileInterface';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
 
 
 
@@ -104,10 +106,25 @@ class Uploader extends React.Component {
 }
 
 
-DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
-
-ReactDOM.render((
-    <Uploader />
-),
-    document.getElementById('root')
-);
\ No newline at end of file
+// DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+(async () => {
+    const info = await CurrentUserUtils.loadCurrentUser();
+    DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + "mobile");
+    await Docs.Prototypes.initialize();
+    if (info.id !== "__guest__") {
+        // a guest will not have an id registered
+        await CurrentUserUtils.loadUserDocument(info);
+    }
+    document.getElementById('root')!.addEventListener('wheel', event => {
+        if (event.ctrlKey) {
+            event.preventDefault();
+        }
+    }, true);
+    ReactDOM.render((
+        // <Uploader />
+        <MobileInterface />
+    ),
+        document.getElementById('root')
+    );
+}
+)();
\ No newline at end of file
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
new file mode 100644
index 000000000..ea2fc917f
--- /dev/null
+++ b/src/mobile/MobileInterface.tsx
@@ -0,0 +1,69 @@
+import React = require('react');
+import { observer } from 'mobx-react';
+import { computed, action } from 'mobx';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
+import { FieldValue, Cast } from '../new_fields/Types';
+import { Doc } from '../new_fields/Doc';
+import { Docs } from '../client/documents/Documents';
+import { CollectionView } from '../client/views/collections/CollectionView';
+import { DocumentView } from '../client/views/nodes/DocumentView';
+import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from '../Utils';
+import { Transform } from '../client/util/Transform';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faPenNib, faHighlighter, faEraser, faMousePointer } from '@fortawesome/free-solid-svg-icons';
+
+@observer
+export default class MobileInterface extends React.Component {
+    @computed private get userDoc() { return CurrentUserUtils.UserDocument; }
+    @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; }
+
+    @action
+    componentDidMount = () => {
+        library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]);
+
+        if (this.userDoc && !this.mainContainer) {
+            const doc = CurrentUserUtils.setupMobileDoc(this.userDoc);
+            this.userDoc.activeMobile = doc;
+        }
+    }
+
+    @computed
+    get mainContent() {
+        if (this.mainContainer) {
+            return <DocumentView
+                Document={this.mainContainer}
+                DataDoc={undefined}
+                LibraryPath={emptyPath}
+                addDocument={undefined}
+                addDocTab={returnFalse}
+                pinToPres={emptyFunction}
+                removeDocument={undefined}
+                ruleProvider={undefined}
+                onClick={undefined}
+                ScreenToLocalTransform={Transform.Identity}
+                ContentScaling={returnOne}
+                PanelWidth={() => window.screen.width}
+                PanelHeight={() => window.screen.height}
+                renderDepth={0}
+                focus={emptyFunction}
+                backgroundColor={returnEmptyString}
+                parentActive={returnTrue}
+                whenActiveChanged={emptyFunction}
+                bringToFront={emptyFunction}
+                ContainingCollectionView={undefined}
+                ContainingCollectionDoc={undefined}
+                zoomToScale={emptyFunction}
+                getScale={returnOne}>
+            </DocumentView>;
+        }
+        return "hello";
+    }
+
+    render() {
+        return (
+            <div className="mobile-container">
+                {this.mainContent}
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 83d631958..20dbf4b17 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -9,7 +9,8 @@ export enum InkTool {
     Pen,
     Highlighter,
     Eraser,
-    Scrubber
+    Scrubber,
+    Stamp
 }
 
 export interface PointData {
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 4edcfa623..062604458 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -37,7 +37,8 @@ export namespace GestureUtils {
     export enum Gestures {
         Box = "box",
         Line = "line",
-        Stroke = "stroke"
+        Stroke = "stroke",
+        Scribble = "scribble"
     }
 
     export const GestureRecognizer = new NDollarRecognizer(false);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 220c37e2b..71dd34e68 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -28,6 +28,7 @@ export class CurrentUserUtils {
 
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestWorkspace: Doc | undefined;
+    @observable public static GuestMobile: Doc | undefined;
 
     // a default set of note types .. not being used yet...
     static setupNoteTypes(doc: Doc) {
@@ -55,8 +56,10 @@ export class CurrentUserUtils {
             { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
             { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+            { title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
             { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
             { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
             { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
             { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
             { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
@@ -90,6 +93,29 @@ export class CurrentUserUtils {
         }
     }
 
+    static setupMobileButtons(doc: Doc, buttons?: string[]) {
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
+            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
+            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
+            { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
+            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
+        ];
+        return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
+            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+        }));
+    }
+
+    static setupMobileDoc(userDoc: Doc) {
+        return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
+            columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
+        });
+    }
+
     // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker.  when clicked, this panel will be displayed in the target container (ie, sidebarContainer)  
     static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
         // setup a masonry view of all he creators
-- 
cgit v1.2.3-70-g09d2


From d86b4db095379d473820271868e8f7cd5830d502 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Mon, 13 Jan 2020 15:25:07 -0500
Subject: palette events are now on gestureOverlay, but cffv and dv is still
 capturing one and two finger events, which makes it impossible for the
 gestureoverlay to capture 5 fingers spread across different targets

---
 src/client/documents/Documents.ts                  |  3 +
 src/client/util/InteractionUtils.ts                | 10 ++-
 src/client/views/GestureOverlay.tsx                | 64 ++++++++++++++-
 src/client/views/InkingControl.tsx                 |  5 +-
 src/client/views/Palette.scss                      | 10 ++-
 src/client/views/Palette.tsx                       | 44 +++++++++-
 src/client/views/Touchable.tsx                     | 21 +++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 94 +++++++++++-----------
 src/client/views/nodes/DocumentView.tsx            | 24 +++++-
 src/new_fields/documentSchemas.ts                  |  2 +
 .../authentication/models/current_user_utils.ts    | 23 ++++++
 11 files changed, 224 insertions(+), 76 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1853be529..64abd4f57 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -103,6 +103,8 @@ export interface DocumentOptions {
     ischecked?: ScriptField; // returns whether a font icon box is checked
     activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
     onClick?: ScriptField;
+    onPointerDown?: ScriptField;
+    onPointerUp?: ScriptField;
     dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
     onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     icon?: string;
@@ -116,6 +118,7 @@ export interface DocumentOptions {
     color?: string;
     limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
     // [key: string]: Opt<Field>;
+    pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
 }
 
 class EmptyBox {
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index 2e4e8c7ca..76b43da3c 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -8,12 +8,14 @@ export namespace InteractionUtils {
     const REACT_POINTER_PEN_BUTTON = 0;
     const ERASER_BUTTON = 5;
 
-    export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] {
+    export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
         const myTouches = new Array<React.Touch>();
         for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt = e.targetTouches.item(i);
+            const pt: any = e.targetTouches.item(i);
             if (pt && prevPoints.has(pt.identifier)) {
-                myTouches.push(pt);
+                if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+                    myTouches.push(pt);
+                }
             }
         }
         return myTouches;
@@ -23,7 +25,7 @@ export namespace InteractionUtils {
         switch (type) {
             // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
             case PENTYPE:
-                return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? POINTER_PEN_BUTTON : REACT_POINTER_PEN_BUTTON);
+                return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
             case ERASERTYPE:
                 return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
             default:
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 848927912..830c06f1f 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,7 +2,7 @@ import React = require("react");
 import { Touchable } from "./Touchable";
 import { observer } from "mobx-react";
 import "./GestureOverlay.scss"
-import { computed, observable, action } from "mobx";
+import { computed, observable, action, runInAction } from "mobx";
 import { CreatePolyline } from "./InkingStroke";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
@@ -12,14 +12,22 @@ import { Doc } from "../../new_fields/Doc";
 import { LinkManager } from "../util/LinkManager";
 import { DocUtils } from "../documents/Documents";
 import { undoBatch } from "../util/UndoManager";
+import { Scripting } from "../util/Scripting";
+import { FieldValue, Cast } from "../../new_fields/Types";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import Palette from "./Palette";
 
 @observer
 export default class GestureOverlay extends Touchable {
     static Instance: GestureOverlay;
 
     @observable private _points: { X: number, Y: number }[] = [];
+    @observable private _palette?: JSX.Element;
+    @observable public Color: string = "rgb(244, 67, 54)";
+    @observable public Width: number = 5;
 
     private _d1: Doc | undefined;
+    private thumbIdentifier?: number;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -27,6 +35,47 @@ export default class GestureOverlay extends Touchable {
         GestureOverlay.Instance = this;
     }
 
+    @action
+    handleHandDown = (e: React.TouchEvent) => {
+        const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+        const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+        this.thumbIdentifier = thumb?.identifier;
+        const others = fingers.filter(f => f !== thumb);
+        const minX = Math.min(...others.map(f => f.clientX));
+        const minY = Math.min(...others.map(f => f.clientY));
+        // const t = this.getTransform().transformPoint(minX, minY);
+        // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+
+        const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+        if (thumbDoc) {
+            this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+        }
+
+        document.removeEventListener("touchmove", this.onTouch);
+        document.removeEventListener("touchmove", this.handleHandMove);
+        document.addEventListener("touchmove", this.handleHandMove);
+        document.removeEventListener("touchend", this.handleHandUp);
+        document.addEventListener("touchend", this.handleHandUp);
+    }
+
+    @action
+    handleHandMove = (e: TouchEvent) => {
+        for (let i = 0; i < e.changedTouches.length; i++) {
+            const pt = e.changedTouches.item(i);
+            if (pt?.identifier === this.thumbIdentifier) {
+            }
+        }
+    }
+
+    @action
+    handleHandUp = (e: TouchEvent) => {
+        this.onTouchEnd(e);
+        if (this.prevPoints.size < 3) {
+            this._palette = undefined;
+            document.removeEventListener("touchend", this.handleHandUp);
+        }
+    }
+
     @action
     onPointerDown = (e: React.PointerEvent) => {
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
@@ -82,6 +131,8 @@ export default class GestureOverlay extends Touchable {
         return actionPerformed;
     }
 
+
+
     @action
     onPointerUp = (e: PointerEvent) => {
         if (this._points.length > 1) {
@@ -157,16 +208,21 @@ export default class GestureOverlay extends Touchable {
 
         return (
             <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
-                {CreatePolyline(this._points, B.left, B.top)}
+                {CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
             </svg>
         );
     }
 
     render() {
         return (
-            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown}>
+            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart}>
                 {this.props.children}
+                {this._palette}
                 {this.currentStroke}
             </div>);
     }
-}
\ No newline at end of file
+}
+
+Scripting.addGlobal("GestureOverlay", GestureOverlay);
+Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { GestureOverlay.Instance.Color = color; GestureOverlay.Instance.Width = width; }); });
+Scripting.addGlobal(function resetPen() { runInAction(() => { GestureOverlay.Instance.Color = "rgb(244, 67, 54)"; GestureOverlay.Instance.Width = 5; }); });
\ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 243123352..be07a9024 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -10,12 +10,13 @@ import { Scripting } from "../util/Scripting";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import GestureOverlay from "./GestureOverlay";
 
 export class InkingControl {
     @observable static Instance: InkingControl;
     @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(CurrentUserUtils.UserDocument.inkTool)) ?? InkTool.None; }
-    @computed private get _selectedColor(): string { return FieldValue(StrCast(CurrentUserUtils.UserDocument.inkColor)) ?? "rgb(244, 67, 54)"; }
-    @computed private get _selectedWidth(): string { return FieldValue(StrCast(CurrentUserUtils.UserDocument.inkWidth)) ?? "5"; }
+    @computed private get _selectedColor(): string { return GestureOverlay.Instance.Color ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkColor)) ?? "rgb(244, 67, 54)"; }
+    @computed private get _selectedWidth(): string { return GestureOverlay.Instance.Width?.toString() ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkWidth)) ?? "5"; }
     @observable public _open: boolean = false;
 
     constructor() {
diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss
index 60004c81f..2626774cb 100644
--- a/src/client/views/Palette.scss
+++ b/src/client/views/Palette.scss
@@ -4,15 +4,17 @@
         height: 300px;
         touch-action: pan-x;
         overflow: scroll;
+        position: absolute;
 
         .palette-thumbContent {
             width: 100%;
             height: 100%;
+        }
 
-            .palette-button {
-                width: 100px;
-                height: 100px;
-            }
+        .palette-button {
+            width: 100px;
+            height: 100px;
+            background: blue;
         }
     }
 }
\ No newline at end of file
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 390b7e2c2..3649cccfe 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -1,22 +1,60 @@
 import * as React from "react";
 import "./Palette.scss";
 import { PointData } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { Docs } from "../documents/Documents";
+import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
+import { List } from "../../new_fields/List";
+import { DocumentView } from "./nodes/DocumentView";
+import { emptyPath, returnFalse, emptyFunction, returnOne, returnEmptyString, returnTrue } from "../../Utils";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Transform } from "../util/Transform";
+import { computed, action } from "mobx";
+import { FieldValue, Cast } from "../../new_fields/Types";
+import { observer } from "mobx-react";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { CollectionStackingView } from "./collections/CollectionStackingView";
+import { CollectionView } from "./collections/CollectionView";
 
 export interface PaletteProps {
     x: number;
     y: number;
     thumb: number[];
+    thumbDoc: Doc;
 }
 
+@observer
 export default class Palette extends React.Component<PaletteProps> {
     render() {
         return (
             <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
                 <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}>
                     <div className="palette-thumbContent">
-                        <div className="palette-button" style={{ background: "green" }} onPointerDown={() => console.log("hi")}>1</div>
-                        <div className="palette-button" style={{ background: "red" }}>2</div>
-                        <div className="palette-button" style={{ background: "blue" }}>3</div>
+                        <DocumentView
+                            Document={this.props.thumbDoc}
+                            DataDoc={undefined}
+                            LibraryPath={emptyPath}
+                            addDocument={undefined}
+                            addDocTab={returnFalse}
+                            pinToPres={emptyFunction}
+                            removeDocument={undefined}
+                            ruleProvider={undefined}
+                            onClick={undefined}
+                            ScreenToLocalTransform={Transform.Identity}
+                            ContentScaling={returnOne}
+                            PanelWidth={() => window.screen.width}
+                            PanelHeight={() => window.screen.height}
+                            renderDepth={0}
+                            focus={emptyFunction}
+                            backgroundColor={returnEmptyString}
+                            parentActive={returnTrue}
+                            whenActiveChanged={emptyFunction}
+                            bringToFront={emptyFunction}
+                            ContainingCollectionView={undefined}
+                            ContainingCollectionDoc={undefined}
+                            zoomToScale={emptyFunction}
+                            getScale={returnOne}>
+                        </DocumentView>
                     </div>
                 </div>
             </div>
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 3eb66ff72..24ea801a0 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -21,15 +21,26 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
      */
     @action
     protected onTouchStart = (e: React.TouchEvent): void => {
+        const actualPts: React.Touch[] = [];
         for (let i = 0; i < e.targetTouches.length; i++) {
             const pt: any = e.targetTouches.item(i);
+            actualPts.push(pt);
             // 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 > 0.5 && pt.radiusY > 0.5) {
+            if (pt.radiusX > 1 && pt.radiusY > 1) {
                 this.prevPoints.set(pt.identifier, pt);
             }
         }
 
+        const ptsToDelete: number[] = [];
+        this.prevPoints.forEach(pt => {
+            if (!actualPts.includes(pt)) {
+                ptsToDelete.push(pt.identifier);
+            }
+        });
+
+        ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+
         if (this.prevPoints.size) {
             switch (this.prevPoints.size) {
                 case 1:
@@ -57,13 +68,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     */
     @action
     protected onTouch = (e: TouchEvent): void => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
 
         // if we're not actually moving a lot, don't consider it as dragging yet
         if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
         this._touchDrag = true;
         if (this.holdTimer) {
-            console.log("clear");
             clearTimeout(this.holdTimer);
             this.holdTimer = undefined;
         }
@@ -99,7 +109,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             }
         }
         if (this.holdTimer) {
-            console.log("clear");
             clearTimeout(this.holdTimer);
             this.holdTimer = undefined;
         }
@@ -152,7 +161,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     }
 
     handleHandDown = (e: React.TouchEvent) => {
-        e.stopPropagation();
-        e.preventDefault();
+        // e.stopPropagation();
+        // e.preventDefault();
     }
 }
\ 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 d7cccc036..84945c6e6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,7 +9,7 @@ import { Id } from "../../../../new_fields/FieldSymbols";
 import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
@@ -468,7 +468,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
         if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
                 if (InkingControl.Instance.selectedTool === InkTool.None) {
@@ -490,7 +490,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle2PointersMove = (e: TouchEvent) => {
         // pinch zooming
         if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
             const pt1 = myTouches[0];
             const pt2 = myTouches[1];
 
@@ -849,51 +849,47 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     private thumbIdentifier?: number;
-    private hand?: React.Touch[];
 
-    @action
-    handleHandDown = (e: React.TouchEvent) => {
-        const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
-        this.hand = fingers;
-        const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
-        this.thumbIdentifier = thumb?.identifier;
-        const others = fingers.filter(f => f !== thumb);
-        const minX = Math.min(...others.map(f => f.clientX));
-        const minY = Math.min(...others.map(f => f.clientY));
-        const t = this.getTransform().transformPoint(minX, minY);
-        const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
-        this._palette = <Palette x={t[0]} y={t[1]} thumb={th} />;
-
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchmove", this.handleHandMove);
-        document.addEventListener("touchmove", this.handleHandMove);
-        document.removeEventListener("touchend", this.handleHandUp);
-        document.addEventListener("touchend", this.handleHandUp);
-    }
-
-    @action
-    handleHandMove = (e: TouchEvent) => {
-        for (let i = 0; i < e.changedTouches.length; i++) {
-            const pt = e.changedTouches.item(i);
-            if (pt?.identifier === this.thumbIdentifier) {
-            }
-        }
-    }
-
-    @action
-    handleHandUp = (e: TouchEvent) => {
-        console.log(e.changedTouches.length);
-        this.onTouchEnd(e);
-        if (this.prevPoints.size < 3) {
-            if (this.hand) {
-                for (const h of this.hand) {
-                    this.prevPoints.has(h.identifier) && this.prevPoints.delete(h.identifier);
-                }
-            }
-            this._palette = undefined;
-            document.removeEventListener("touchend", this.handleHandUp);
-        }
-    }
+    // @action
+    // handleHandDown = (e: React.TouchEvent) => {
+    //     const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+    //     const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+    //     this.thumbIdentifier = thumb?.identifier;
+    //     const others = fingers.filter(f => f !== thumb);
+    //     const minX = Math.min(...others.map(f => f.clientX));
+    //     const minY = Math.min(...others.map(f => f.clientY));
+    //     const t = this.getTransform().transformPoint(minX, minY);
+    //     const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+
+    //     const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+    //     if (thumbDoc) {
+    //         this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />;
+    //     }
+
+    //     document.removeEventListener("touchmove", this.onTouch);
+    //     document.removeEventListener("touchmove", this.handleHandMove);
+    //     document.addEventListener("touchmove", this.handleHandMove);
+    //     document.removeEventListener("touchend", this.handleHandUp);
+    //     document.addEventListener("touchend", this.handleHandUp);
+    // }
+
+    // @action
+    // handleHandMove = (e: TouchEvent) => {
+    //     for (let i = 0; i < e.changedTouches.length; i++) {
+    //         const pt = e.changedTouches.item(i);
+    //         if (pt?.identifier === this.thumbIdentifier) {
+    //         }
+    //     }
+    // }
+
+    // @action
+    // handleHandUp = (e: TouchEvent) => {
+    //     this.onTouchEnd(e);
+    //     if (this.prevPoints.size < 3) {
+    //         this._palette = undefined;
+    //         document.removeEventListener("touchend", this.handleHandUp);
+    //     }
+    // }
 
     onContextMenu = (e: React.MouseEvent) => {
         const layoutItems: ContextMenuProps[] = [];
@@ -958,12 +954,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         ];
     }
 
-    @observable private _palette?: JSX.Element;
+    // @observable private _palette?: JSX.Element;
 
     children = () => {
         const eles: JSX.Element[] = [];
         this.extensionDoc && (eles.push(...this.childViews()));
-        this._palette && (eles.push(this._palette));
+        // this._palette && (eles.push(this._palette));
         // this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
         return eles;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 415d0e2ef..bdec94eb3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -59,6 +59,8 @@ export interface DocumentViewProps {
     LibraryPath: Doc[];
     fitToBox?: boolean;
     onClick?: ScriptField;
+    onPointerDown?: ScriptField;
+    onPointerUp?: ScriptField;
     dragDivName?: string;
     addDocument?: (doc: Doc) => boolean;
     removeDocument?: (doc: Doc) => boolean;
@@ -105,6 +107,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
     @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
     @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+    @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
+    @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
 
     @action
     componentDidMount() {
@@ -180,7 +184,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
-    onClick = async (e: React.MouseEvent) => {
+    onClick = async (e: React.MouseEvent | React.PointerEvent) => {
         if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
             (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
             e.stopPropagation();
@@ -240,8 +244,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     handle1PointerDown = (e: React.TouchEvent) => {
+        if (this.Document.onPointerDown) return;
         if (!e.nativeEvent.cancelBubble) {
-            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0];
+            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
             this._downX = touch.clientX;
             this._downY = touch.clientY;
             this._hitTemplateDrag = false;
@@ -265,7 +270,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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) {
-            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0];
+            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[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);
@@ -293,7 +298,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     @action
     handle2PointersMove = (e: TouchEvent) => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
         const pt1 = myTouches[0];
         const pt2 = myTouches[1];
         const oldPoint1 = this.prevPoints.get(pt1.identifier);
@@ -363,6 +368,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
+        if (this.onPointerDownHandler && this.onPointerDownHandler.script) {
+            this.onPointerDownHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            document.addEventListener("pointerup", this.onPointerUp);
+            return;
+        }
         // 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)
@@ -412,6 +423,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     onPointerUp = (e: PointerEvent): void => {
+        if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+            this.onPointerUpHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            return;
+        }
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
         this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 21e69fbed..1cba3cba9 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -20,6 +20,8 @@ export const documentSchema = createSchema({
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    onPointerDown: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    onPointerUp: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     ignoreAspect: "boolean",    // whether aspect ratio should be ignored when laying out or manipulating the document
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 71dd34e68..2ade6f102 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -110,6 +110,29 @@ export class CurrentUserUtils {
         }));
     }
 
+    static setupThumbButtons(doc: Doc) {
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
+            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+        ];
+        return docProtoData.map(data => Docs.Create.FontIconDocument({
+            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
+            onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
+            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+        }));
+    }
+
+    static setupThumbDoc(userDoc: Doc) {
+        if (!userDoc.thumbDoc) {
+            return Docs.Create.MasonryDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
+                width: 300, columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
+            });
+        }
+        return userDoc.thumbDoc;
+    }
+
     static setupMobileDoc(userDoc: Doc) {
         return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
             columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
-- 
cgit v1.2.3-70-g09d2


From de0c00ff0bb1f58a0736da24acc984f5a090d009 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Tue, 14 Jan 2020 18:35:28 -0500
Subject: ok we now use custom events for touch. im struggling with figuring
 out how to ignore a hand lol

---
 src/client/util/InteractionUtils.ts                |  50 ++++++-
 src/client/views/CollectionLinearView.tsx          |   4 +-
 src/client/views/GestureOverlay.tsx                | 160 +++++++++++++++++++--
 src/client/views/Touchable.tsx                     | 114 ++++++++++-----
 .../views/collections/CollectionStackingView.tsx   |   6 +-
 src/client/views/collections/CollectionSubView.tsx |   8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  80 ++++++-----
 src/client/views/nodes/DocumentView.tsx            |  32 +++--
 src/pen-gestures/GestureUtils.ts                   |   2 +-
 9 files changed, 344 insertions(+), 112 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 76b43da3c..63ee2fb92 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -8,13 +8,53 @@ export namespace InteractionUtils {
     const REACT_POINTER_PEN_BUTTON = 0;
     const ERASER_BUTTON = 5;
 
+    export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
+        constructor(
+            readonly fingers: number,
+            readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+            readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
+        ) { }
+    }
+
+    export interface MultiTouchEventDisposer { (): void; }
+
+    export function MakeMultiTouchTarget(
+        element: HTMLElement,
+        startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
+    ): MultiTouchEventDisposer {
+        const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
+        // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+        // if (onMultiTouchMoveHandler) {
+        //     element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+        // }
+        // if (onMultiTouchEndHandler) {
+        //     element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
+        // }
+        return () => {
+            element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+            // if (onMultiTouchMoveHandler) {
+            //     element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+            // }
+            // if (onMultiTouchEndHandler) {
+            //     element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
+            // }
+        };
+    }
+
     export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
         const myTouches = new Array<React.Touch>();
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt: any = e.targetTouches.item(i);
-            if (pt && prevPoints.has(pt.identifier)) {
-                if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
-                    myTouches.push(pt);
+        for (let i = 0; i < e.touches.length; i++) {
+            const pt: any = e.touches.item(i);
+            if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+                for (let j = 0; j < e.targetTouches.length; j++) {
+                    const tPt = e.targetTouches.item(j);
+                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+                        if (pt && prevPoints.has(pt.identifier)) {
+                            myTouches.push(pt);
+                        }
+                    }
                 }
             }
         }
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 5d6a58656..38f2decdd 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -36,7 +36,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
             { fireImmediately: true }
         );
     }
-    protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+    protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
         this._dropDisposer && this._dropDisposer();
         if (ele) {
             this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
@@ -55,7 +55,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     render() {
         const guid = Utils.GenerateGuid();
         return <div className="collectionLinearView-outer">
-            <div className="collectionLinearView" ref={this.createDropAndGestureTarget} >
+            <div className="collectionLinearView" ref={this.createDashEventsTarget} >
                 <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
                     onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 830c06f1f..a5fba362c 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,7 @@
 import React = require("react");
 import { Touchable } from "./Touchable";
 import { observer } from "mobx-react";
-import "./GestureOverlay.scss"
+import "./GestureOverlay.scss";
 import { computed, observable, action, runInAction } from "mobx";
 import { CreatePolyline } from "./InkingStroke";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
@@ -16,6 +16,7 @@ import { Scripting } from "../util/Scripting";
 import { FieldValue, Cast } from "../../new_fields/Types";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 import Palette from "./Palette";
+import { Utils } from "../../Utils";
 
 @observer
 export default class GestureOverlay extends Touchable {
@@ -28,6 +29,9 @@ export default class GestureOverlay extends Touchable {
 
     private _d1: Doc | undefined;
     private thumbIdentifier?: number;
+    private _hands: (React.Touch[])[] = [];
+
+    protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -35,11 +39,143 @@ export default class GestureOverlay extends Touchable {
         GestureOverlay.Instance = this;
     }
 
-    @action
+    getNewTouches(e: React.TouchEvent | TouchEvent) {
+        const ntt: (React.Touch | Touch)[] = [];
+        const nct: (React.Touch | Touch)[] = [];
+        const nt: (React.Touch | Touch)[] = [];
+        this._hands.forEach((hand) => {
+            for (let i = 0; i < e.targetTouches.length; i++) {
+                const pt = e.targetTouches.item(i);
+                if (pt && !hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    ntt.push(pt);
+                }
+            }
+
+            for (let i = 0; i < e.changedTouches.length; i++) {
+                const pt = e.changedTouches.item(i);
+                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nct.push(pt);
+                }
+            }
+
+            for (let i = 0; i < e.changedTouches.length; i++) {
+                const pt = e.touches.item(i);
+                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nt.push(pt);
+                }
+            }
+        });
+        return { ntt, nct, nt };
+    }
+
+    onReactTouchStart = (te: React.TouchEvent) => {
+        const actualPts: React.Touch[] = [];
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt: any = te.touches.item(i);
+            actualPts.push(pt);
+            // 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 > 1 && pt.radiusY > 1) {
+                // if (typeof pt.identifier !== "string") {
+                //     pt.identifier = Utils.GenerateGuid();
+                // }
+                this.prevPoints.set(pt.identifier, pt);
+            }
+        }
+
+        const ptsToDelete: number[] = [];
+        this.prevPoints.forEach(pt => {
+            if (!actualPts.includes(pt)) {
+                ptsToDelete.push(pt.identifier);
+            }
+        });
+
+        ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+
+        if (this.prevPoints.size && this.prevPoints.size < 5) {
+            const nts: any = this.getNewTouches(te);
+            const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
+            target?.dispatchEvent(
+                new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
+                    {
+                        bubbles: true,
+                        detail: {
+                            fingers: this.prevPoints.size,
+                            points: te.targetTouches,
+                            touchEvent: new React.TouchEvent("start", {
+                                targetTouches: nts.ntt,
+                                touches: nts.nt,
+                                changedTouches: nts.nct
+                            });
+                        }
+                    }
+                )
+            );
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+            document.addEventListener("touchmove", this.onReactTouchMove);
+            document.addEventListener("touchend", this.onReactTouchEnd);
+        }
+        else {
+            this.handleHandDown(te);
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+        }
+    }
+
+    onReactTouchMove = (e: TouchEvent) => {
+        const nts: any = this.getNewTouches(e);
+        document.dispatchEvent(
+            new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove",
+                {
+                    bubbles: true,
+                    detail: {
+                        fingers: this.prevPoints.size,
+                        points: e.touches,
+                        touchEvent: new TouchEvent("move", {
+                            targetTouches: nts.ntt,
+                            changedTouches: nts.nct,
+                            touches: nts.nt,
+                        })
+                    }
+                })
+        );
+    }
+
+    onReactTouchEnd = (e: TouchEvent) => {
+        document.dispatchEvent(
+            new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd",
+                {
+                    bubbles: true,
+                    detail: {
+                        fingers: this.prevPoints.size,
+                        points: e.touches,
+                        touchEvent: e
+                    }
+                })
+        );
+        for (let i = 0; i < e.changedTouches.length; i++) {
+            const pt = e.changedTouches.item(i);
+            if (pt) {
+                if (this.prevPoints.has(pt.identifier)) {
+                    this.prevPoints.delete(pt.identifier);
+                }
+            }
+        }
+
+        if (this.prevPoints.size === 0) {
+            document.removeEventListener("touchmove", this.onReactTouchMove);
+            document.removeEventListener("touchend", this.onReactTouchEnd);
+        }
+        e.stopPropagation();
+    }
+
     handleHandDown = (e: React.TouchEvent) => {
         const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
         const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
         this.thumbIdentifier = thumb?.identifier;
+        fingers.forEach((f) => this.prevPoints.delete(f.identifier));
+        this._hands.push(fingers);
         const others = fingers.filter(f => f !== thumb);
         const minX = Math.min(...others.map(f => f.clientX));
         const minY = Math.min(...others.map(f => f.clientY));
@@ -48,10 +184,12 @@ export default class GestureOverlay extends Touchable {
 
         const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
         if (thumbDoc) {
-            this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+            runInAction(() => {
+                this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+            });
         }
 
-        document.removeEventListener("touchmove", this.onTouch);
+        this.removeMoveListeners();
         document.removeEventListener("touchmove", this.handleHandMove);
         document.addEventListener("touchmove", this.handleHandMove);
         document.removeEventListener("touchend", this.handleHandUp);
@@ -69,11 +207,9 @@ export default class GestureOverlay extends Touchable {
 
     @action
     handleHandUp = (e: TouchEvent) => {
-        this.onTouchEnd(e);
-        if (this.prevPoints.size < 3) {
-            this._palette = undefined;
-            document.removeEventListener("touchend", this.handleHandUp);
-        }
+        // this.onTouchEnd(e);
+        this._palette = undefined;
+        document.removeEventListener("touchend", this.handleHandUp);
     }
 
     @action
@@ -115,7 +251,7 @@ export default class GestureOverlay extends Touchable {
                 DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
                 actionPerformed = true;
             }
-        }
+        };
         const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
             {
                 bubbles: true,
@@ -181,7 +317,7 @@ export default class GestureOverlay extends Touchable {
                             }
                         }
                     )
-                )
+                );
                 this._points = [];
             }
         }
@@ -215,7 +351,7 @@ export default class GestureOverlay extends Touchable {
 
     render() {
         return (
-            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart}>
+            <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
                 {this.props.children}
                 {this._palette}
                 {this.currentStroke}
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 2a7599fbf..a71015b05 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -8,8 +8,11 @@ const HOLD_DURATION = 1000;
 
 export abstract class Touchable<T = {}> extends React.Component<T> {
     //private holdTimer: NodeJS.Timeout | undefined;
-    holdTimer: NodeJS.Timeout | undefined;
+    private holdTimer: NodeJS.Timeout | undefined;
+    private moveDisposer?: InteractionUtils.MultiTouchEventDisposer;
+    private endDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
+    protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     protected _touchDrag: boolean = false;
     protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
 
@@ -22,16 +25,27 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
      * When a touch even starts, we keep track of each touch that is associated with that event
      */
     @action
-    protected onTouchStart = (e: React.TouchEvent): void => {
+    protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
         const actualPts: React.Touch[] = [];
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt: any = e.targetTouches.item(i);
+        const te = me.touchEvent;
+        // loop through all touches on screen
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt: any = te.touches.item(i);
             actualPts.push(pt);
-            // 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 > 1 && pt.radiusY > 1) {
+            if (this.prevPoints.has(pt.identifier)) {
                 this.prevPoints.set(pt.identifier, pt);
             }
+            // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives
+            for (let j = 0; j < te.changedTouches.length; j++) {
+                const tPt = te.changedTouches.item(j);
+                if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) {
+                    // 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 > 1 && pt.radiusY > 1) {
+                        this.prevPoints.set(pt.identifier, pt);
+                    }
+                }
+            }
         }
 
         const ptsToDelete: number[] = [];
@@ -41,26 +55,30 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             }
         });
 
+        // console.log(ptsToDelete.length);
         ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
 
+        // console.log(this.prevPoints.size);
         if (this.prevPoints.size) {
             switch (this.prevPoints.size) {
                 case 1:
-                    this.handle1PointerDown(e);
-                    e.persist();
+                    this.handle1PointerDown(te);
+                    te.persist();
                     // if (this.holdTimer) {
                     //     clearTimeout(this.holdTimer)
                     //     this.holdTimer = undefined;
                     // }
-                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION);
+                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te), HOLD_DURATION);
+                    // e.stopPropagation();
                     // console.log(this.holdTimer);
                     break;
                 case 2:
-                    this.handle2PointersDown(e);
-                    break;
-                case 5:
-                    this.handleHandDown(e);
+                    this.handle2PointersDown(te);
+                    // e.stopPropagation();
                     break;
+                // case 5:
+                //     this.handleHandDown(te);
+                //     break;
             }
         }
     }
@@ -69,28 +87,30 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     * Handle touch move event
     */
     @action
-    protected onTouch = (e: TouchEvent): void => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+    protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+        const te = me.touchEvent;
+        const myTouches = InteractionUtils.GetMyTargetTouches(te, this.prevPoints, true);
 
         // if we're not actually moving a lot, don't consider it as dragging yet
         if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
         this._touchDrag = true;
         if (this.holdTimer) {
-            console.log("CLEAR")
+            console.log("CLEAR");
             clearTimeout(this.holdTimer);
             // this.holdTimer = undefined;
         }
+        // console.log(myTouches.length);
         switch (myTouches.length) {
             case 1:
-                this.handle1PointerMove(e);
+                this.handle1PointerMove(te);
                 break;
             case 2:
-                this.handle2PointersMove(e);
+                this.handle2PointersMove(te);
                 break;
         }
 
-        for (let i = 0; i < e.targetTouches.length; i++) {
-            const pt = e.targetTouches.item(i);
+        for (let i = 0; i < te.touches.length; i++) {
+            const pt = te.touches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.set(pt.identifier, pt);
@@ -100,11 +120,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     }
 
     @action
-    protected onTouchEnd = (e: TouchEvent): void => {
+    protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
         // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up");
         // remove all the touches associated with the event
-        for (let i = 0; i < e.changedTouches.length; i++) {
-            const pt = e.changedTouches.item(i);
+        const te = me.touchEvent;
+        for (let i = 0; i < te.changedTouches.length; i++) {
+            const pt = te.changedTouches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.delete(pt.identifier);
@@ -116,7 +137,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
             console.log("clear");
         }
         this._touchDrag = false;
-        e.stopPropagation();
+        te.stopPropagation();
 
 
         // if (e.targetTouches.length === 0) {
@@ -126,11 +147,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         if (this.prevPoints.size === 0) {
             this.cleanUpInteractions();
         }
+        e.stopPropagation();
     }
 
     cleanUpInteractions = (): void => {
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     handle1PointerMove = (e: TouchEvent): any => {
@@ -144,23 +166,43 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     }
 
     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);
+        this.removeMoveListeners();
+        this.addMoveListeners();
+        this.removeEndListeners();
+        this.addEndListeners();
     }
 
     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);
+        this.removeMoveListeners();
+        this.addMoveListeners();
+        this.removeEndListeners();
+        this.addEndListeners();
     }
 
     handle1PointerHoldStart = (e: React.TouchEvent): any => {
         e.stopPropagation();
         e.preventDefault();
-        document.removeEventListener("touchmove", this.onTouch);
+        this.removeMoveListeners();
+    }
+
+    addMoveListeners = () => {
+        const handler = (e: Event) => this.onTouch(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+        document.addEventListener("dashOnTouchMove", handler);
+        this.moveDisposer = () => document.removeEventListener("dashOnTouchMove", handler);
+    }
+
+    removeMoveListeners = () => {
+        this.moveDisposer && this.moveDisposer();
+    }
+
+    addEndListeners = () => {
+        const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+        document.addEventListener("dashOnTouchEnd", handler);
+        this.endDisposer = () => document.removeEventListener("dashOnTouchEnd", handler);
+    }
+
+    removeEndListeners = () => {
+        this.endDisposer && this.endDisposer();
     }
 
     handleHandDown = (e: React.TouchEvent) => {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 992820fc7..b8423af20 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -153,7 +153,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     createRef = (ele: HTMLDivElement | null) => {
         this._masonryGridRef = ele;
-        this.createDropAndGestureTarget(ele!); //so the whole grid is the drop target?
+        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
     }
 
     overlays = (doc: Doc) => {
@@ -309,7 +309,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             docList={docList}
             parent={this}
             type={type}
-            createDropTarget={this.createDropAndGestureTarget}
+            createDropTarget={this.createDashEventsTarget}
             screenToLocalTransform={this.props.ScreenToLocalTransform}
         />;
     }
@@ -342,7 +342,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             docList={docList}
             parent={this}
             type={type}
-            createDropTarget={this.createDropAndGestureTarget}
+            createDropTarget={this.createDashEventsTarget}
             screenToLocalTransform={this.props.ScreenToLocalTransform}
             setDocHeight={this.setDocHeight}
         />;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 73dc7edc6..4133956fc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -24,6 +24,7 @@ import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { Networking } from "../../Network";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -49,17 +50,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
     class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
         private dropDisposer?: DragManager.DragDropDisposer;
         private gestureDisposer?: GestureUtils.GestureEventDisposer;
+        protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
         private _childLayoutDisposer?: IReactionDisposer;
-        protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+        protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
             this.dropDisposer && this.dropDisposer();
             this.gestureDisposer && this.gestureDisposer();
+            this.multiTouchDisposer && this.multiTouchDisposer();
             if (ele) {
                 this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
                 this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
+                this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
             }
         }
         protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
-            this.createDropAndGestureTarget(ele);
+            this.createDashEventsTarget(ele);
         }
 
         componentDidMount() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 84945c6e6..3894e9d63 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -326,29 +326,30 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     handle1PointerDown = (e: React.TouchEvent) => {
-        const 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();
-                //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
-                //     this._points.push({ X: point[0], Y: point[1] });
-                // }
-                if (InkingControl.Instance.selectedTool === InkTool.None) {
-                    this._lastX = pt.pageX;
-                    this._lastY = pt.pageY;
-                    e.stopPropagation();
-                    e.preventDefault();
-                }
-                else {
-                    e.stopPropagation();
-                    e.preventDefault();
+        if (!e.nativeEvent.cancelBubble) {
+            // const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const pt = e.changedTouches.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)) {
+                    this.removeMoveListeners();
+                    this.addMoveListeners();
+                    this.removeEndListeners();
+                    this.addEndListeners();
+                    // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+                    //     e.stopPropagation();
+                    //     e.preventDefault();
+                    //     const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
+                    //     this._points.push({ X: point[0], Y: point[1] });
+                    // }
+                    if (InkingControl.Instance.selectedTool === InkTool.None) {
+                        this._lastX = pt.pageX;
+                        this._lastY = pt.pageY;
+                        e.preventDefault();
+                    }
+                    else {
+                        e.preventDefault();
+                    }
                 }
             }
         }
@@ -395,8 +396,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     @action
@@ -482,7 +483,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     this.pan(pt);
                 }
             }
-            e.stopPropagation();
+            // e.stopPropagation();
             e.preventDefault();
         }
     }
@@ -527,7 +528,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     }
                 }
             }
-            e.stopPropagation();
+            // e.stopPropagation();
             e.preventDefault();
         }
     }
@@ -535,18 +536,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     handle2PointersDown = (e: React.TouchEvent) => {
         if (!e.nativeEvent.cancelBubble && this.props.active(true)) {
-            const pt1: React.Touch | null = e.targetTouches.item(0);
-            const pt2: React.Touch | null = e.targetTouches.item(1);
-            if (!pt1 || !pt2) return;
+            // const pt1: React.Touch | null = e.targetTouches.item(0);
+            // const pt2: React.Touch | null = e.targetTouches.item(1);
+            // // if (!pt1 || !pt2) return;
+            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const pt1 = myTouches[0];
+            const pt2 = myTouches[1];
 
             const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
             const 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);
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
             e.stopPropagation();
         }
     }
@@ -554,8 +558,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     cleanUpInteractions = () => {
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
-        document.removeEventListener("touchmove", this.onTouch);
-        document.removeEventListener("touchend", this.onTouchEnd);
+        this.removeMoveListeners();
+        this.removeEndListeners();
     }
 
     @action
@@ -989,9 +993,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
         if (!this.extensionDoc) return (null);
         // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
-        return <div className={"collectionfreeformview-container"} ref={this.createDropAndGestureTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+        return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
             style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
-            onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}>
+            onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
             {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2e0ae09ba..b33bebe7d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -103,6 +103,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
     private _titleRef = React.createRef<EditableView>();
 
+    protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
     public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
     public get ContentDiv() { return this._mainCont.current; }
     @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
@@ -177,6 +179,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     componentDidMount() {
         this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
         this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+        this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
 
         !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this);
     }
@@ -184,7 +187,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     componentDidUpdate() {
         this._dropDisposer && this._dropDisposer();
+        this._gestureEventDisposer && this._gestureEventDisposer();
+        this.multiTouchDisposer && this.multiTouchDisposer();
         this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
+        this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+        this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
     }
 
     @action
@@ -319,25 +326,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 }
             }
             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();
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
+            e.stopPropagation();
         }
     }
 
     handle1PointerMove = (e: TouchEvent) => {
         if ((e as any).formattedHandled) { e.stopPropagation; return; }
         if (e.cancelBubble && this.active) {
-            document.removeEventListener("touchmove", this.onTouch);
+            this.removeMoveListeners();
         }
         else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
             const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[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.cleanUpInteractions();
                     this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
                 }
             }
@@ -352,10 +358,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             e.stopPropagation();
             e.preventDefault();
 
-            document.removeEventListener("touchmove", this.onTouch);
-            document.addEventListener("touchmove", this.onTouch);
-            document.removeEventListener("touchend", this.onTouchEnd);
-            document.addEventListener("touchend", this.onTouchEnd);
+            this.removeMoveListeners();
+            this.addMoveListeners();
+            this.removeEndListeners();
+            this.addEndListeners();
         }
     }
 
@@ -970,7 +976,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 width: animwidth,
                 height: animheight,
                 opacity: this.Document.opacity
-            }} onTouchStart={this.onTouchStart}>
+            }}>
             {this.innards}
         </div>;
     }
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 062604458..4b5ad6684 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -31,7 +31,7 @@ export namespace GestureUtils {
         element.addEventListener("dashOnGesture", handler);
         return () => {
             element.removeEventListener("dashOnGesture", handler);
-        }
+        };
     }
 
     export enum Gestures {
-- 
cgit v1.2.3-70-g09d2


From 19be14b5807b117f477b57b9ecc7b96247bf4fef Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Wed, 15 Jan 2020 13:30:08 -0500
Subject: the quick brown fox jumped over the lazy dog

---
 src/client/util/InteractionUtils.ts                | 13 ++--
 src/client/views/CollectionLinearView.tsx          | 14 +++-
 src/client/views/GestureOverlay.tsx                | 50 ++++++-------
 src/client/views/Palette.tsx                       |  4 ++
 src/client/views/Touchable.tsx                     | 35 ++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 41 +++++------
 src/client/views/nodes/DocumentView.tsx            | 82 +++++++++++-----------
 .../authentication/models/current_user_utils.ts    |  2 +-
 8 files changed, 126 insertions(+), 115 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 63ee2fb92..da42bdc93 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -11,7 +11,10 @@ export namespace InteractionUtils {
     export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
         constructor(
             readonly fingers: number,
-            readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+            // readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+            readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+            readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+            readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
             readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
         ) { }
     }
@@ -43,13 +46,11 @@ export namespace InteractionUtils {
         };
     }
 
-    export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
+    export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
         const myTouches = new Array<React.Touch>();
-        for (let i = 0; i < e.touches.length; i++) {
-            const pt: any = e.touches.item(i);
+        for (const pt of mte.touches) {
             if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
-                for (let j = 0; j < e.targetTouches.length; j++) {
-                    const tPt = e.targetTouches.item(j);
+                for (const tPt of mte.targetTouches) {
                     if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
                         if (pt && prevPoints.has(pt.identifier)) {
                             myTouches.push(pt);
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 38f2decdd..2262a3c0c 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -1,4 +1,4 @@
-import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
@@ -21,12 +21,15 @@ const LinearDocument = makeInterface(documentSchema);
 @observer
 export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     @observable public addMenuToggle = React.createRef<HTMLInputElement>();
+    @observable private _selectedIndex = -1;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _widthDisposer?: IReactionDisposer;
+    private _selectedDisposer?: IReactionDisposer;
 
     componentWillUnmount() {
         this._dropDisposer && this._dropDisposer();
         this._widthDisposer && this._widthDisposer();
+        this._selectedDisposer && this._selectedDisposer();
     }
 
     componentDidMount() {
@@ -35,6 +38,12 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
             () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
             { fireImmediately: true }
         );
+
+        this._selectedDisposer = reaction(
+            () => NumCast(this.props.Document.selectedIndex),
+            (i) => runInAction(() => this._selectedIndex = i),
+            { fireImmediately: true }
+        );
     }
     protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
         this._dropDisposer && this._dropDisposer();
@@ -61,11 +70,12 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
 
                 <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
-                    {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
+                    {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
                         const nested = pair.layout.viewType === CollectionViewType.Linear;
                         const dref = React.createRef<HTMLDivElement>();
                         const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
                         const deltaSize = nativeWidth * .15 / 2;
+                        const isSelected = this._selectedIndex === ind;
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
                             style={{
                                 width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index a5fba362c..e4c14f6b8 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -40,28 +40,28 @@ export default class GestureOverlay extends Touchable {
     }
 
     getNewTouches(e: React.TouchEvent | TouchEvent) {
-        const ntt: (React.Touch | Touch)[] = [];
-        const nct: (React.Touch | Touch)[] = [];
-        const nt: (React.Touch | Touch)[] = [];
+        const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches);
+        const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches);
+        const nt: (React.Touch | Touch)[] = Array.from(e.touches);
         this._hands.forEach((hand) => {
             for (let i = 0; i < e.targetTouches.length; i++) {
                 const pt = e.targetTouches.item(i);
-                if (pt && !hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    ntt.push(pt);
+                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    ntt.splice(ntt.indexOf(pt));
                 }
             }
 
             for (let i = 0; i < e.changedTouches.length; i++) {
                 const pt = e.changedTouches.item(i);
-                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    nct.push(pt);
+                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nct.splice(nct.indexOf(pt));
                 }
             }
 
-            for (let i = 0; i < e.changedTouches.length; i++) {
+            for (let i = 0; i < e.touches.length; i++) {
                 const pt = e.touches.item(i);
-                if (pt && !hand.some((finger: React.Touch) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    nt.push(pt);
+                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
+                    nt.splice(ntt.indexOf(pt));
                 }
             }
         });
@@ -93,7 +93,7 @@ export default class GestureOverlay extends Touchable {
         ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
 
         if (this.prevPoints.size && this.prevPoints.size < 5) {
-            const nts: any = this.getNewTouches(te);
+            const nts = this.getNewTouches(te);
             const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
             target?.dispatchEvent(
                 new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
@@ -101,12 +101,10 @@ export default class GestureOverlay extends Touchable {
                         bubbles: true,
                         detail: {
                             fingers: this.prevPoints.size,
-                            points: te.targetTouches,
-                            touchEvent: new React.TouchEvent("start", {
-                                targetTouches: nts.ntt,
-                                touches: nts.nt,
-                                changedTouches: nts.nct
-                            });
+                            targetTouches: nts.ntt,
+                            touches: nts.nt,
+                            changedTouches: nts.nct,
+                            touchEvent: te
                         }
                     }
                 )
@@ -131,25 +129,26 @@ export default class GestureOverlay extends Touchable {
                     bubbles: true,
                     detail: {
                         fingers: this.prevPoints.size,
-                        points: e.touches,
-                        touchEvent: new TouchEvent("move", {
-                            targetTouches: nts.ntt,
-                            changedTouches: nts.nct,
-                            touches: nts.nt,
-                        })
+                        targetTouches: nts.ntt,
+                        touches: nts.nt,
+                        changedTouches: nts.nct,
+                        touchEvent: e
                     }
                 })
         );
     }
 
     onReactTouchEnd = (e: TouchEvent) => {
+        const nts: any = this.getNewTouches(e);
         document.dispatchEvent(
             new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd",
                 {
                     bubbles: true,
                     detail: {
                         fingers: this.prevPoints.size,
-                        points: e.touches,
+                        targetTouches: nts.ntt,
+                        touches: nts.nt,
+                        changedTouches: nts.nct,
                         touchEvent: e
                     }
                 })
@@ -171,7 +170,7 @@ export default class GestureOverlay extends Touchable {
     }
 
     handleHandDown = (e: React.TouchEvent) => {
-        const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+        const fingers = Array.from(e.touches);
         const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
         this.thumbIdentifier = thumb?.identifier;
         fingers.forEach((f) => this.prevPoints.delete(f.identifier));
@@ -201,6 +200,7 @@ export default class GestureOverlay extends Touchable {
         for (let i = 0; i < e.changedTouches.length; i++) {
             const pt = e.changedTouches.item(i);
             if (pt?.identifier === this.thumbIdentifier) {
+                console.log("thumby movey")
             }
         }
     }
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 3649cccfe..aca1811a4 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -15,6 +15,9 @@ import { observer } from "mobx-react";
 import { DocumentContentsView } from "./nodes/DocumentContentsView";
 import { CollectionStackingView } from "./collections/CollectionStackingView";
 import { CollectionView } from "./collections/CollectionView";
+import { CollectionSubView, SubCollectionViewProps } from "./collections/CollectionSubView";
+import { makeInterface } from "../../new_fields/Schema";
+import { documentSchema } from "../../new_fields/documentSchemas";
 
 export interface PaletteProps {
     x: number;
@@ -25,6 +28,7 @@ export interface PaletteProps {
 
 @observer
 export default class Palette extends React.Component<PaletteProps> {
+
     render() {
         return (
             <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index a71015b05..7800c4019 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -29,15 +29,13 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         const actualPts: React.Touch[] = [];
         const te = me.touchEvent;
         // loop through all touches on screen
-        for (let i = 0; i < te.touches.length; i++) {
-            const pt: any = te.touches.item(i);
+        for (const pt of me.touches) {
             actualPts.push(pt);
             if (this.prevPoints.has(pt.identifier)) {
                 this.prevPoints.set(pt.identifier, pt);
             }
             // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives
-            for (let j = 0; j < te.changedTouches.length; j++) {
-                const tPt = te.changedTouches.item(j);
+            for (const tPt of me.changedTouches) {
                 if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) {
                     // 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
@@ -58,22 +56,21 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         // console.log(ptsToDelete.length);
         ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
 
-        // console.log(this.prevPoints.size);
         if (this.prevPoints.size) {
             switch (this.prevPoints.size) {
                 case 1:
-                    this.handle1PointerDown(te);
+                    this.handle1PointerDown(te, me);
                     te.persist();
                     // if (this.holdTimer) {
                     //     clearTimeout(this.holdTimer)
                     //     this.holdTimer = undefined;
                     // }
-                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te), HOLD_DURATION);
+                    this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te, me), HOLD_DURATION);
                     // e.stopPropagation();
                     // console.log(this.holdTimer);
                     break;
                 case 2:
-                    this.handle2PointersDown(te);
+                    this.handle2PointersDown(te, me);
                     // e.stopPropagation();
                     break;
                 // case 5:
@@ -89,7 +86,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     @action
     protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
         const te = me.touchEvent;
-        const myTouches = InteractionUtils.GetMyTargetTouches(te, this.prevPoints, true);
+        const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
 
         // if we're not actually moving a lot, don't consider it as dragging yet
         if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
@@ -102,15 +99,14 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         // console.log(myTouches.length);
         switch (myTouches.length) {
             case 1:
-                this.handle1PointerMove(te);
+                this.handle1PointerMove(te, me);
                 break;
             case 2:
-                this.handle2PointersMove(te);
+                this.handle2PointersMove(te, me);
                 break;
         }
 
-        for (let i = 0; i < te.touches.length; i++) {
-            const pt = te.touches.item(i);
+        for (const pt of me.touches) {
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.set(pt.identifier, pt);
@@ -124,8 +120,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up");
         // remove all the touches associated with the event
         const te = me.touchEvent;
-        for (let i = 0; i < te.changedTouches.length; i++) {
-            const pt = te.changedTouches.item(i);
+        for (const pt of me.changedTouches) {
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.delete(pt.identifier);
@@ -155,31 +150,31 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         this.removeEndListeners();
     }
 
-    handle1PointerMove = (e: TouchEvent): any => {
+    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
         e.stopPropagation();
         e.preventDefault();
     }
 
-    handle2PointersMove = (e: TouchEvent): any => {
+    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
         e.stopPropagation();
         e.preventDefault();
     }
 
-    handle1PointerDown = (e: React.TouchEvent): any => {
+    handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
         this.removeMoveListeners();
         this.addMoveListeners();
         this.removeEndListeners();
         this.addEndListeners();
     }
 
-    handle2PointersDown = (e: React.TouchEvent): any => {
+    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
         this.removeMoveListeners();
         this.addMoveListeners();
         this.removeEndListeners();
         this.addEndListeners();
     }
 
-    handle1PointerHoldStart = (e: React.TouchEvent): any => {
+    handle1PointerHoldStart = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
         e.stopPropagation();
         e.preventDefault();
         this.removeMoveListeners();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3894e9d63..aaa585b55 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -44,7 +44,7 @@ import { TraceMobx } from "../../../../new_fields/util";
 import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
 import { LinkManager } from "../../../util/LinkManager";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import Palette from "../../Palette";
+import CollectionPaletteVIew from "../../Palette";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -276,7 +276,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+        if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
             return;
         }
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -325,10 +325,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    handle1PointerDown = (e: React.TouchEvent) => {
+    handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
         if (!e.nativeEvent.cancelBubble) {
             // const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
-            const pt = e.changedTouches.item(0);
+            const pt = me.changedTouches[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)) {
@@ -466,10 +466,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    handle1PointerMove = (e: TouchEvent) => {
+    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         // panning a workspace
         if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = myTouches[0];
             if (pt) {
                 if (InkingControl.Instance.selectedTool === InkTool.None) {
@@ -488,10 +488,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    handle2PointersMove = (e: TouchEvent) => {
+    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         // pinch zooming
         if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt1 = myTouches[0];
             const pt2 = myTouches[1];
 
@@ -534,24 +534,25 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    handle2PointersDown = (e: React.TouchEvent) => {
+    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
         if (!e.nativeEvent.cancelBubble && this.props.active(true)) {
             // const pt1: React.Touch | null = e.targetTouches.item(0);
             // const pt2: React.Touch | null = e.targetTouches.item(1);
             // // if (!pt1 || !pt2) return;
-            const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt1 = myTouches[0];
             const pt2 = myTouches[1];
-
-            const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-            const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-            this._lastX = centerX;
-            this._lastY = centerY;
-            this.removeMoveListeners();
-            this.addMoveListeners();
-            this.removeEndListeners();
-            this.addEndListeners();
-            e.stopPropagation();
+            if (pt1 && pt2) {
+                const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                this._lastX = centerX;
+                this._lastY = centerY;
+                this.removeMoveListeners();
+                this.addMoveListeners();
+                this.removeEndListeners();
+                this.addEndListeners();
+                e.stopPropagation();
+            }
         }
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b33bebe7d..0b6a284d6 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -134,46 +134,46 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     //     document.addEventListener("touchend", this.handle1PointerHoldEnd);
     // }
 
-    handle1PointerHoldMove = (e: TouchEvent): void => {
-        const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
-        if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
-            this.handleRelease();
-        }
-        document.removeEventListener("touchmove", this.handle1PointerHoldMove);
-        document.addEventListener("touchmove", this.handle1PointerHoldMove);
-        document.removeEventListener("touchend", this.handle1PointerHoldEnd);
-        document.addEventListener("touchend", this.handle1PointerHoldEnd);
-    }
+    // handle1PointerHoldMove = (e: TouchEvent): void => {
+    //     const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+    //     if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
+    //         this.handleRelease();
+    //     }
+    //     document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+    //     document.addEventListener("touchmove", this.handle1PointerHoldMove);
+    //     document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+    //     document.addEventListener("touchend", this.handle1PointerHoldEnd);
+    // }
 
-    handleRelease() {
-        RadialMenu.Instance.closeMenu();
-        document.removeEventListener("touchmove", this.handle1PointerHoldMove);
-        document.removeEventListener("touchend", this.handle1PointerHoldEnd);
-    }
+    // handleRelease() {
+    //     RadialMenu.Instance.closeMenu();
+    //     document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+    //     document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+    // }
 
-    handle1PointerHoldEnd = (e: TouchEvent): void => {
-        RadialMenu.Instance.closeMenu();
-        document.removeEventListener("touchmove", this.handle1PointerHoldMove);
-        document.removeEventListener("touchend", this.handle1PointerHoldEnd);
-    }
+    // handle1PointerHoldEnd = (e: TouchEvent): void => {
+    //     RadialMenu.Instance.closeMenu();
+    //     document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+    //     document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+    // }
 
-    @action
-    onRadialMenu = (e: React.TouchEvent): void => {
-        const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
+    // @action
+    // onRadialMenu = (e: React.TouchEvent): void => {
+    //     const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
 
-        RadialMenu.Instance.openMenu();
+    //     RadialMenu.Instance.openMenu();
 
-        RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group", selected: -1 });
-        RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
-        RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "folder", selected: -1 });
-        RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
+    //     RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group", selected: -1 });
+    //     RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
+    //     RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "folder", selected: -1 });
+    //     RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
 
-        RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15);
-        if (!SelectionManager.IsSelected(this, true)) {
-            SelectionManager.SelectDoc(this, false);
-        }
-        e.stopPropagation();
-    }
+    //     RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15);
+    //     if (!SelectionManager.IsSelected(this, true)) {
+    //         SelectionManager.SelectDoc(this, false);
+    //     }
+    //     e.stopPropagation();
+    // }
 
     @action
     componentDidMount() {
@@ -313,10 +313,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
-    handle1PointerDown = (e: React.TouchEvent) => {
+    handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
         if (this.Document.onPointerDown) return;
         if (!e.nativeEvent.cancelBubble) {
-            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
+            const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
             this._downX = touch.clientX;
             this._downY = touch.clientY;
             this._hitTemplateDrag = false;
@@ -334,13 +334,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
-    handle1PointerMove = (e: TouchEvent) => {
+    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
         if ((e as any).formattedHandled) { e.stopPropagation; return; }
         if (e.cancelBubble && this.active) {
             this.removeMoveListeners();
         }
         else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
-            const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
+            const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[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)) {
                     this.cleanUpInteractions();
@@ -353,7 +353,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
-    handle2PointersDown = (e: React.TouchEvent) => {
+    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
         if (!e.nativeEvent.cancelBubble && !this.isSelected()) {
             e.stopPropagation();
             e.preventDefault();
@@ -366,8 +366,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     @action
-    handle2PointersMove = (e: TouchEvent) => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+        const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
         const pt1 = myTouches[0];
         const pt2 = myTouches[1];
         const oldPoint1 = this.prevPoints.get(pt1.identifier);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 2ade6f102..a850b0f9f 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -126,7 +126,7 @@ export class CurrentUserUtils {
 
     static setupThumbDoc(userDoc: Doc) {
         if (!userDoc.thumbDoc) {
-            return Docs.Create.MasonryDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
+            return Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
                 width: 300, columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
             });
         }
-- 
cgit v1.2.3-70-g09d2


From 4b0a056f1afaf20dea4be64c7b238748d99ad12e Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Fri, 17 Jan 2020 15:36:22 -0500
Subject: palette now doesn't interfere with touch

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/util/InteractionUtils.ts                |  2 +-
 src/client/views/GestureOverlay.tsx                | 82 +++++++++++++++++-----
 src/client/views/MainView.tsx                      |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../authentication/models/current_user_utils.ts    |  1 +
 6 files changed, 70 insertions(+), 23 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64abd4f57..c49642de0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -119,6 +119,7 @@ export interface DocumentOptions {
     limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
     // [key: string]: Opt<Field>;
     pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
+    isExpanded?: boolean; // is linear view expanded
 }
 
 class EmptyBox {
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index da42bdc93..3e6d27242 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -49,7 +49,7 @@ export namespace InteractionUtils {
     export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
         const myTouches = new Array<React.Touch>();
         for (const pt of mte.touches) {
-            if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+            if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
                 for (const tPt of mte.targetTouches) {
                     if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
                         if (pt && prevPoints.has(pt.identifier)) {
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 96c1513ff..854dff0e2 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -16,7 +16,8 @@ import { Scripting } from "../util/Scripting";
 import { FieldValue, Cast, NumCast } from "../../new_fields/Types";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 import Palette from "./Palette";
-import { Utils } from "../../Utils";
+import { Utils, emptyPath, emptyFunction } from "../../Utils";
+import { DocumentView } from "./nodes/DocumentView";
 
 @observer
 export default class GestureOverlay extends Touchable {
@@ -24,15 +25,15 @@ export default class GestureOverlay extends Touchable {
 
     @observable private _points: { X: number, Y: number }[] = [];
     @observable private _palette?: JSX.Element;
+    @observable private _elements: JSX.Element[];
     @observable public Color: string = "rgb(244, 67, 54)";
     @observable public Width: number = 5;
 
     private _d1: Doc | undefined;
     private _thumbDoc: Doc | undefined;
     private _thumbX?: number;
-    private _thumbY?: number;
     private thumbIdentifier?: number;
-    private _hands: (React.Touch[])[] = [];
+    private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
 
     protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
 
@@ -49,22 +50,22 @@ export default class GestureOverlay extends Touchable {
         this._hands.forEach((hand) => {
             for (let i = 0; i < e.targetTouches.length; i++) {
                 const pt = e.targetTouches.item(i);
-                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    ntt.splice(ntt.indexOf(pt));
+                if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+                    ntt.splice(ntt.indexOf(pt), 1);
                 }
             }
 
             for (let i = 0; i < e.changedTouches.length; i++) {
                 const pt = e.changedTouches.item(i);
-                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    nct.splice(nct.indexOf(pt));
+                if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+                    nct.splice(nct.indexOf(pt), 1);
                 }
             }
 
             for (let i = 0; i < e.touches.length; i++) {
                 const pt = e.touches.item(i);
-                if (pt && hand.some((finger) => finger.screenX === pt?.screenX && finger.screenY === pt.screenY)) {
-                    nt.splice(ntt.indexOf(pt));
+                if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+                    nt.splice(nt.indexOf(pt), 1);
                 }
             }
         });
@@ -94,9 +95,10 @@ export default class GestureOverlay extends Touchable {
         });
 
         ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+        const nts = this.getNewTouches(te);
+        console.log(nts.nt.length);
 
-        if (this.prevPoints.size && this.prevPoints.size < 5) {
-            const nts = this.getNewTouches(te);
+        if (nts.nt.length < 5) {
             const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
             target?.dispatchEvent(
                 new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
@@ -190,12 +192,12 @@ export default class GestureOverlay extends Touchable {
         const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
         if (thumb.identifier === this.thumbIdentifier) {
             this._thumbX = thumb.clientX;
-            this._thumbY = thumb.clientY;
+            this._hands.set(thumb.identifier, fingers);
             return;
         }
         this.thumbIdentifier = thumb?.identifier;
-        fingers.forEach((f) => this.prevPoints.delete(f.identifier));
-        this._hands.push(fingers);
+        // fingers.forEach((f) => this.prevPoints.delete(f.identifier));
+        this._hands.set(thumb.identifier, fingers);
         const others = fingers.filter(f => f !== thumb);
         const minX = Math.min(...others.map(f => f.clientX));
         const minY = Math.min(...others.map(f => f.clientY));
@@ -207,7 +209,6 @@ export default class GestureOverlay extends Touchable {
             runInAction(() => {
                 this._thumbDoc = thumbDoc;
                 this._thumbX = thumb.clientX;
-                this._thumbY = thumb.clientY;
                 this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
             });
         }
@@ -221,6 +222,29 @@ export default class GestureOverlay extends Touchable {
 
     @action
     handleHandMove = (e: TouchEvent) => {
+        const fingers = new Array<React.Touch>();
+        for (let i = 0; i < e.touches.length; i++) {
+            const pt: any = e.touches.item(i);
+            if (pt.radiusX > 1 && pt.radiusY > 1) {
+                for (let j = 0; j < e.targetTouches.length; j++) {
+                    const tPt = e.targetTouches.item(j);
+                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+                        if (pt && this.prevPoints.has(pt.identifier)) {
+                            this._hands.forEach(hand => hand.some(f => {
+                                if (f.identifier === pt.identifier) {
+                                    fingers.push(pt);
+                                }
+                            }));
+                        }
+                    }
+                }
+            }
+        }
+        const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+        if (thumb?.identifier === this.thumbIdentifier) {
+            this._hands.set(thumb.identifier, fingers);
+        }
+
         for (let i = 0; i < e.changedTouches.length; i++) {
             const pt = e.changedTouches.item(i);
             if (pt && pt.identifier === this.thumbIdentifier && this._thumbX && this._thumbDoc) {
@@ -236,6 +260,7 @@ export default class GestureOverlay extends Touchable {
     handleHandUp = (e: TouchEvent) => {
         if (e.touches.length < 3) {
             // this.onTouchEnd(e);
+            if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier);
             this._palette = undefined;
             this.thumbIdentifier = undefined;
             this._thumbDoc = undefined;
@@ -380,16 +405,35 @@ export default class GestureOverlay extends Touchable {
         );
     }
 
+    @computed get elements() {
+        return [
+            this.props.children,
+            this._elements,
+            this._palette,
+            this.currentStroke
+        ]
+    }
+
+    @action
+    openFloatingDoc = (doc: Doc) => {
+        // this._elements.push(
+        //     <DocumentView 
+        //     Document={doc}
+        //     LibraryPath={emptyPath}
+        //     addDocument={undefined}
+        //     addDocTab={emptyFunction}
+        //     />
+        // )
+    }
+
     render() {
         return (
             <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
-                {this.props.children}
-                {this._palette}
-                {this.currentStroke}
+                {this.elements}
             </div>);
     }
 }
 
 Scripting.addGlobal("GestureOverlay", GestureOverlay);
 Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { GestureOverlay.Instance.Color = color; GestureOverlay.Instance.Width = width; }); });
-Scripting.addGlobal(function resetPen() { runInAction(() => { GestureOverlay.Instance.Color = "rgb(244, 67, 54)"; GestureOverlay.Instance.Width = 5; }); });
\ No newline at end of file
+Scripting.addGlobal(function resetPen() { runInAction(() => { runInAction(() => { GestureOverlay.Instance.Color = "rgb(244, 67, 54)"; GestureOverlay.Instance.Width = 5; })); });
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 93fb0a07c..b8b956270 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
     faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
-    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp
+    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -137,6 +137,7 @@ export class MainView extends React.Component {
         library.add(faEllipsisV);
         library.add(faMusic);
         library.add(faPhone);
+        library.add(faClipboard);
         library.add(faStamp);
         this.initEventListeners();
         this.initAuthenticationRouters();
@@ -518,7 +519,7 @@ export class MainView extends React.Component {
             </GestureOverlay>
             <PreviewCursor />
             <ContextMenu />
-            <RadialMenu/>
+            <RadialMenu />
             <PDFMenu />
             <MarqueeOptionsMenu />
             <OverlayView />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aaa585b55..b302bb1dc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -327,7 +327,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
         if (!e.nativeEvent.cancelBubble) {
-            // const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+            // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
             const pt = me.changedTouches[0];
             if (pt) {
                 this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 8b8b1e029..eb48b28f2 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -114,6 +114,7 @@ export class CurrentUserUtils {
         const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
             { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            // { title: "notepad", icon: "clipboard", pointerUp: "closeFloatingDoc(this.clipboard)", pointerDown: 'openFloatingDoc(this.clipboard)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
             nativeWidth: 10, nativeHeight: 10, width: 10, height: 10, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
-- 
cgit v1.2.3-70-g09d2


From 3bb4d8324b6101a82122ecb31f025c2e0420df89 Mon Sep 17 00:00:00 2001
From: Stanley Yip <stanley_yip@brown.edu>
Date: Fri, 17 Jan 2020 16:22:38 -0500
Subject: moving around the createPolyline function

---
 src/client/util/InteractionUtils.ts                | 206 -------------------
 src/client/util/InteractionUtils.tsx               | 220 +++++++++++++++++++++
 src/client/views/GestureOverlay.tsx                |   3 +-
 src/client/views/InkingStroke.tsx                  |  17 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   1 -
 5 files changed, 223 insertions(+), 224 deletions(-)
 delete mode 100644 src/client/util/InteractionUtils.ts
 create mode 100644 src/client/util/InteractionUtils.tsx

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

diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
deleted file mode 100644
index 3e6d27242..000000000
--- a/src/client/util/InteractionUtils.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-export namespace InteractionUtils {
-    export const MOUSETYPE = "mouse";
-    export const TOUCHTYPE = "touch";
-    export const PENTYPE = "pen";
-    export const ERASERTYPE = "eraser";
-
-    const POINTER_PEN_BUTTON = -1;
-    const REACT_POINTER_PEN_BUTTON = 0;
-    const ERASER_BUTTON = 5;
-
-    export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
-        constructor(
-            readonly fingers: number,
-            // readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
-            readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
-            readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
-        ) { }
-    }
-
-    export interface MultiTouchEventDisposer { (): void; }
-
-    export function MakeMultiTouchTarget(
-        element: HTMLElement,
-        startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
-    ): MultiTouchEventDisposer {
-        const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
-        // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
-        // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
-        element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
-        // if (onMultiTouchMoveHandler) {
-        //     element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
-        // }
-        // if (onMultiTouchEndHandler) {
-        //     element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
-        // }
-        return () => {
-            element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
-            // if (onMultiTouchMoveHandler) {
-            //     element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
-            // }
-            // if (onMultiTouchEndHandler) {
-            //     element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
-            // }
-        };
-    }
-
-    export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
-        const myTouches = new Array<React.Touch>();
-        for (const pt of mte.touches) {
-            if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
-                for (const tPt of mte.targetTouches) {
-                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
-                        if (pt && prevPoints.has(pt.identifier)) {
-                            myTouches.push(pt);
-                        }
-                    }
-                }
-            }
-        }
-        return myTouches;
-    }
-
-    export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
-        switch (type) {
-            // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
-            case PENTYPE:
-                return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
-            case ERASERTYPE:
-                return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
-            default:
-                return e.pointerType === type;
-        }
-    }
-
-    export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
-        return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
-    }
-
-    /**
-     * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
-     * @param pts - n-arbitrary long list of points
-     */
-    export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
-        const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
-        const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
-        return { X: centerX, Y: centerY };
-    }
-
-    /**
-     * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in
-     * @param pt1 - new point that corresponds to oldPoint1
-     * @param pt2 - new point that corresponds to oldPoint2
-     * @param oldPoint1 - previous point 1
-     * @param oldPoint2 - previous point 2
-     */
-    export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        const threshold = 4;
-        const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
-        const newDist = TwoPointEuclidist(pt1, pt2);
-
-        /** if they have the same sign, then we are either pinching in or out.
-          * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
-          * so that it can still pan without freaking out
-          */
-        if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
-            return Math.sign(oldDist - newDist);
-        }
-        return 0;
-    }
-
-    /**
-     * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in
-     * @param pt1 - new point that corresponds to oldPoint1
-     * @param pt2 - new point that corresponds to oldPoint2
-     * @param oldPoint1 - previous point 1
-     * @param oldPoint2 - previous point 2
-     */
-    export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        const threshold = 4;
-
-        const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
-        const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
-
-        const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
-
-        if (pinching !== 0) {
-            if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
-                return pinching;
-            }
-        }
-        return 0;
-    }
-
-    export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean {
-        for (const touch of newTouches) {
-            if (touch) {
-                const oldTouch = oldTouches.get(touch.identifier);
-                if (oldTouch) {
-                    if (TwoPointEuclidist(touch, oldTouch) >= leniency) {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    // These might not be very useful anymore, but I'll leave them here for now -syip2
-    {
-
-
-        /**
-         * Returns the type of Touch Interaction from a list of points.
-         * Also returns any data that is associated with a Touch Interaction
-         * @param pts - List of points
-         */
-        // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } {
-        //     const leniency = 200;
-        //     switch (pts.length) {
-        //         case 1:
-        //             return { type: OneFinger };
-        //         case 2:
-        //             return { type: TwoSeperateFingers };
-        //         case 3:
-        //             let pt1 = pts[0];
-        //             let pt2 = pts[1];
-        //             let pt3 = pts[2];
-        //             if (pt1 && pt2 && pt3) {
-        //                 let dist12 = TwoPointEuclidist(pt1, pt2);
-        //                 let dist23 = TwoPointEuclidist(pt2, pt3);
-        //                 let dist13 = TwoPointEuclidist(pt1, pt3);
-        //                 console.log(`distances: ${dist12}, ${dist23}, ${dist13}`);
-        //                 let dist12close = dist12 < leniency;
-        //                 let dist23close = dist23 < leniency;
-        //                 let dist13close = dist13 < leniency;
-        //                 let xor2313 = dist23close ? !dist13close : dist13close;
-        //                 let xor = dist12close ? !xor2313 : xor2313;
-        //                 // three input xor because javascript doesn't have logical xor's
-        //                 if (xor) {
-        //                     let points: number[] = [];
-        //                     let min = Math.min(dist12, dist23, dist13);
-        //                     switch (min) {
-        //                         case dist12:
-        //                             points = [0, 1, 2];
-        //                             break;
-        //                         case dist23:
-        //                             points = [1, 2, 0];
-        //                             break;
-        //                         case dist13:
-        //                             points = [0, 2, 1];
-        //                             break;
-        //                     }
-        //                     return { type: TwoToOneFingers, data: points };
-        //                 }
-        //                 else {
-        //                     return { type: ThreeSeperateFingers, data: null };
-        //                 }
-        //             }
-        //         default:
-        //             return { type: undefined };
-        //     }
-        // }
-    }
-}
\ No newline at end of file
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
new file mode 100644
index 000000000..1fe95474c
--- /dev/null
+++ b/src/client/util/InteractionUtils.tsx
@@ -0,0 +1,220 @@
+export namespace InteractionUtils {
+    export const MOUSETYPE = "mouse";
+    export const TOUCHTYPE = "touch";
+    export const PENTYPE = "pen";
+    export const ERASERTYPE = "eraser";
+
+    const POINTER_PEN_BUTTON = -1;
+    const REACT_POINTER_PEN_BUTTON = 0;
+    const ERASER_BUTTON = 5;
+
+    export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+        const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+        return (
+            <polyline
+                points={pts}
+                style={{
+                    fill: "none",
+                    stroke: color,
+                    strokeWidth: width
+                }}
+            />
+        );
+    }
+
+    export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
+        constructor(
+            readonly fingers: number,
+            // readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+            readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+            readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+            readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+            readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
+        ) { }
+    }
+
+    export interface MultiTouchEventDisposer { (): void; }
+
+    export function MakeMultiTouchTarget(
+        element: HTMLElement,
+        startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
+    ): MultiTouchEventDisposer {
+        const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
+        // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+        element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+        // if (onMultiTouchMoveHandler) {
+        //     element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+        // }
+        // if (onMultiTouchEndHandler) {
+        //     element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
+        // }
+        return () => {
+            element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+            // if (onMultiTouchMoveHandler) {
+            //     element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+            // }
+            // if (onMultiTouchEndHandler) {
+            //     element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
+            // }
+        };
+    }
+
+    export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
+        const myTouches = new Array<React.Touch>();
+        for (const pt of mte.touches) {
+            if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+                for (const tPt of mte.targetTouches) {
+                    if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+                        if (pt && prevPoints.has(pt.identifier)) {
+                            myTouches.push(pt);
+                        }
+                    }
+                }
+            }
+        }
+        return myTouches;
+    }
+
+    export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
+        switch (type) {
+            // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
+            case PENTYPE:
+                return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+            case ERASERTYPE:
+                return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+            default:
+                return e.pointerType === type;
+        }
+    }
+
+    export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number {
+        return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2));
+    }
+
+    /**
+     * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
+     * @param pts - n-arbitrary long list of points
+     */
+    export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+        const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
+        const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
+        return { X: centerX, Y: centerY };
+    }
+
+    /**
+     * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in
+     * @param pt1 - new point that corresponds to oldPoint1
+     * @param pt2 - new point that corresponds to oldPoint2
+     * @param oldPoint1 - previous point 1
+     * @param oldPoint2 - previous point 2
+     */
+    export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+        const threshold = 4;
+        const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
+        const newDist = TwoPointEuclidist(pt1, pt2);
+
+        /** if they have the same sign, then we are either pinching in or out.
+          * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+          * so that it can still pan without freaking out
+          */
+        if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
+            return Math.sign(oldDist - newDist);
+        }
+        return 0;
+    }
+
+    /**
+     * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in
+     * @param pt1 - new point that corresponds to oldPoint1
+     * @param pt2 - new point that corresponds to oldPoint2
+     * @param oldPoint1 - previous point 1
+     * @param oldPoint2 - previous point 2
+     */
+    export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
+        const threshold = 4;
+
+        const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
+        const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
+
+        const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+        if (pinching !== 0) {
+            if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
+                return pinching;
+            }
+        }
+        return 0;
+    }
+
+    export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean {
+        for (const touch of newTouches) {
+            if (touch) {
+                const oldTouch = oldTouches.get(touch.identifier);
+                if (oldTouch) {
+                    if (TwoPointEuclidist(touch, oldTouch) >= leniency) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    // These might not be very useful anymore, but I'll leave them here for now -syip2
+    {
+
+
+        /**
+         * Returns the type of Touch Interaction from a list of points.
+         * Also returns any data that is associated with a Touch Interaction
+         * @param pts - List of points
+         */
+        // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } {
+        //     const leniency = 200;
+        //     switch (pts.length) {
+        //         case 1:
+        //             return { type: OneFinger };
+        //         case 2:
+        //             return { type: TwoSeperateFingers };
+        //         case 3:
+        //             let pt1 = pts[0];
+        //             let pt2 = pts[1];
+        //             let pt3 = pts[2];
+        //             if (pt1 && pt2 && pt3) {
+        //                 let dist12 = TwoPointEuclidist(pt1, pt2);
+        //                 let dist23 = TwoPointEuclidist(pt2, pt3);
+        //                 let dist13 = TwoPointEuclidist(pt1, pt3);
+        //                 console.log(`distances: ${dist12}, ${dist23}, ${dist13}`);
+        //                 let dist12close = dist12 < leniency;
+        //                 let dist23close = dist23 < leniency;
+        //                 let dist13close = dist13 < leniency;
+        //                 let xor2313 = dist23close ? !dist13close : dist13close;
+        //                 let xor = dist12close ? !xor2313 : xor2313;
+        //                 // three input xor because javascript doesn't have logical xor's
+        //                 if (xor) {
+        //                     let points: number[] = [];
+        //                     let min = Math.min(dist12, dist23, dist13);
+        //                     switch (min) {
+        //                         case dist12:
+        //                             points = [0, 1, 2];
+        //                             break;
+        //                         case dist23:
+        //                             points = [1, 2, 0];
+        //                             break;
+        //                         case dist13:
+        //                             points = [0, 2, 1];
+        //                             break;
+        //                     }
+        //                     return { type: TwoToOneFingers, data: points };
+        //                 }
+        //                 else {
+        //                     return { type: ThreeSeperateFingers, data: null };
+        //                 }
+        //             }
+        //         default:
+        //             return { type: undefined };
+        //     }
+        // }
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 3de901ff5..ab26687d7 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,6 @@ import { Touchable } from "./Touchable";
 import { observer } from "mobx-react";
 import "./GestureOverlay.scss";
 import { computed, observable, action, runInAction } from "mobx";
-import { CreatePolyline } from "./InkingStroke";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
 import { InkingControl } from "./InkingControl";
@@ -400,7 +399,7 @@ export default class GestureOverlay extends Touchable {
 
         return (
             <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
-                {CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
+                {InteractionUtils.CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
             </svg>
         );
     }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8b346d5d9..aca507147 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -10,24 +10,11 @@ import "./InkingStroke.scss";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import React = require("react");
 import { TraceMobx } from "../../new_fields/util";
+import { InteractionUtils } from "../util/InteractionUtils";
 
 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) {
-    const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
-    return (
-        <polyline
-            points={pts}
-            style={{
-                fill: "none",
-                stroke: color ?? InkingControl.Instance.selectedColor,
-                strokeWidth: width ?? InkingControl.Instance.selectedWidth
-            }}
-        />
-    );
-}
-
 @observer
 export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocument>(InkDocument) {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
@@ -44,7 +31,7 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
         const top = Math.min(...ys);
         const right = Math.max(...xs);
         const bottom = Math.max(...ys);
-        const points = CreatePolyline(data, left, top, this.Document.color, this.Document.strokeWidth);
+        const points = InteractionUtils.CreatePolyline(data, left, top, this.Document.color ?? InkingControl.Instance.selectedColor, this.Document.strokeWidth ?? parseInt(InkingControl.Instance.selectedWidth));
         const width = right - left;
         const height = bottom - top;
         const scaleX = this.PanelWidth / width;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b5e0ce9f0..01b978c81 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -26,7 +26,6 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingControl } from "../../InkingControl";
-import { CreatePolyline } from "../../InkingStroke";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
-- 
cgit v1.2.3-70-g09d2

