From fb9f08295c10c35c11b647d00a3c830867dc3f7d Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Tue, 3 Dec 2019 18:54:41 -0500 Subject: nothing new, infrastructure stuffs --- src/pen-gestures/GestureUtils.ts | 35 +++ src/pen-gestures/ndollar.ts | 529 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 src/pen-gestures/GestureUtils.ts create mode 100644 src/pen-gestures/ndollar.ts (limited to 'src/pen-gestures') diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts new file mode 100644 index 000000000..caaa0736d --- /dev/null +++ b/src/pen-gestures/GestureUtils.ts @@ -0,0 +1,35 @@ +import { NDollarRecognizer } from "./ndollar"; +import { Type } from "typescript"; +import { InkField } from "../new_fields/InkField"; +import { Docs } from "../client/documents/Documents"; +import { Doc } from "../new_fields/Doc"; + +export namespace GestureUtils { + namespace GestureDataTypes { + export type BoxData = Doc[]; + } + + export const GestureRecognizer = new NDollarRecognizer(false); + + export enum Gestures { + Box = "box" + } + + export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] { + switch (name) { + case Gestures.Box: + if (gestureData as GestureDataTypes.BoxData) { + return BoxOptions(gestureData as GestureDataTypes.BoxData); + } + break; + } + throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts"); + } + + function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] { + if (gestureData instanceof Doc[]) { + return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})]; + } + return []; + } +} \ No newline at end of file diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts new file mode 100644 index 000000000..5d4384202 --- /dev/null +++ b/src/pen-gestures/ndollar.ts @@ -0,0 +1,529 @@ +import { GestureUtils } from "./GestureUtils"; + +/** + * The $N Multistroke Recognizer (JavaScript version) + * Converted to TypeScript -syip2 + * + * Lisa Anthony, Ph.D. + * UMBC + * Information Systems Department + * 1000 Hilltop Circle + * Baltimore, MD 21250 + * lanthony@umbc.edu + * + * Jacob O. Wobbrock, Ph.D. + * The Information School + * University of Washington + * Seattle, WA 98195-2840 + * wobbrock@uw.edu + * + * The academic publications for the $N recognizer, and what should be + * used to cite it, are: + * + * Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke + * recognizer for user interface prototypes. Proceedings of Graphics + * Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto, + * Ontario: Canadian Information Processing Society, pp. 245-252. + * https://dl.acm.org/citation.cfm?id=1839258 + * + * Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and + * accurate multistroke recognizer. Proceedings of Graphics Interface + * (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario: + * Canadian Information Processing Society, pp. 117-120. + * https://dl.acm.org/citation.cfm?id=2305296 + * + * The Protractor enhancement was separately published by Yang Li and programmed + * here by Jacob O. Wobbrock and Lisa Anthony: + * + * Li, Y. (2010). Protractor: A fast and accurate gesture + * recognizer. Proceedings of the ACM Conference on Human + * Factors in Computing Systems (CHI '10). Atlanta, Georgia + * (April 10-15, 2010). New York: ACM Press, pp. 2169-2172. + * https://dl.acm.org/citation.cfm?id=1753654 + * + * This software is distributed under the "New BSD License" agreement: + * + * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony. + * All rights reserved. Last updated July 14, 2018. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the names of UMBC nor the University of Washington, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. +**/ + +// +// Point class +// +export class Point { + constructor(public X: number, public Y: number) { } +} + +// +// Rectangle class +// +export class Rectangle { + constructor(public X: number, public Y: number, public Width: number, public Height: number) { } +} + +// +// Unistroke class: a unistroke template +// +export class Unistroke { + public Points: Point[]; + public StartUnitVector: Point; + public Vector: number[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) { + this.Points = Resample(points, NumPoints); + var radians = IndicativeAngle(this.Points); + this.Points = RotateBy(this.Points, -radians); + this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold); + if (useBoundedRotationInvariance) { + this.Points = RotateBy(this.Points, +radians); // restore + } + this.Points = TranslateTo(this.Points, Origin); + this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex); + this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor + } +} +// +// Multistroke class: a container for unistrokes +// +export class Multistroke { + public NumStrokes: number; + public Unistrokes: Unistroke[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor + { + this.NumStrokes = strokes.length; // number of individual strokes + + var order = new Array(strokes.length); // array of integer indices + for (var i = 0; i < strokes.length; i++) { + order[i] = i; // initialize + } + var orders = new Array(); // array of integer arrays + HeapPermute(strokes.length, order, /*out*/ orders); + + var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays + this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke + for (var j = 0; j < unistrokes.length; j++) { + this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]); + } + } +} + +// +// Result class +// +export class Result { + constructor(public Name: string, public Score: any, public Time: any) { } +} + +// +// NDollarRecognizer constants +// +const NumMultistrokes = 1; +const NumPoints = 96; +const SquareSize = 250.0; +const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) +const Origin = new Point(0, 0); +const Diagonal = Math.sqrt(SquareSize * SquareSize + SquareSize * SquareSize); +const HalfDiagonal = 0.5 * Diagonal; +const AngleRange = Deg2Rad(45.0); +const AnglePrecision = Deg2Rad(2.0); +const Phi = 0.5 * (-1.0 + Math.sqrt(5.0)); // Golden Ratio +const StartAngleIndex = (NumPoints / 8); // eighth of gesture length +const AngleSimilarityThreshold = Deg2Rad(30.0); + +// +// NDollarRecognizer class +// +export class NDollarRecognizer { + public Multistrokes: Multistroke[]; + + constructor(useBoundedRotationInvariance: boolean) // constructor + { + // + // one predefined multistroke for each multistroke type + // + this.Multistrokes = new Array(NumMultistrokes); + this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( + new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146)) + )); + + // + // PREDEFINED STROKES + // + + // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 7), new Point(103, 7)), + // new Array(new Point(66, 7), new Point(66, 87)) + // )); + // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 92), new Point(177, 2)), + // new Array(new Point(182, 1), new Point(246, 95)), + // new Array(new Point(247, 87), new Point(247, 1)) + // )); + // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( + // new Array(new Point(345, 9), new Point(345, 87)), + // new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) + // )); + // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( + // new Array(new Point(507, 8), new Point(507, 87)), + // new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) + // )); + // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 146), new Point(106, 222)), + // new Array(new Point(30, 225), new Point(106, 146)) + // )); + // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( + // new Array(new Point(188, 137), new Point(188, 225)), + // new Array(new Point(188, 180), new Point(241, 180)), + // new Array(new Point(241, 137), new Point(241, 225)) + // )); + // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( + // new Array(new Point(371, 149), new Point(371, 221)), + // new Array(new Point(341, 149), new Point(401, 149)), + // new Array(new Point(341, 221), new Point(401, 221)) + // )); + // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( + // new Array(new Point(526, 142), new Point(526, 204)), + // new Array(new Point(526, 221)) + // )); + // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( + // new Array(new Point(12, 347), new Point(119, 347)) + // )); + // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) + // )); + // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( + // new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), + // new Array(new Point(418, 309), new Point(337, 390)) + // )); + // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( + // new Array(new Point(506, 349), new Point(574, 349)), + // new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) + // )); + // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( + // new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), + // new Array(new Point(62, 464), new Point(62, 571)) + // )); + // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), + // new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) + // )); + // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( + // new Array(new Point(325, 499), new Point(417, 557)), + // new Array(new Point(417, 499), new Point(325, 557)), + // new Array(new Point(371, 486), new Point(371, 571)) + // )); + // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( + // new Array(new Point(546, 465), new Point(546, 531)), + // new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) + // )); + // + // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures() + // + } + + Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => { + var t0 = Date.now(); + var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes + var candidate = new Unistroke("", useBoundedRotationInvariance, points); + + var u = -1; + var b = +Infinity; + for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template + { + if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes + { + for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke + { + if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction + { + var d; + if (useProtractor) { + d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor + } + else { + d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N) + } + if (d < b) { + b = d; // best (least) distance + u = i; // multistroke owner of unistroke + } + } + } + } + } + var t1 = Date.now(); + return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); + } + + AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { + this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes); + var num = 0; + for (var i = 0; i < this.Multistrokes.length; i++) { + if (this.Multistrokes[i].Name == name) { + num++; + } + } + return num; + } + + DeleteUserGestures = () => { + this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set + return NumMultistrokes; + } +} + + +// +// Private helper functions from here on down +// +function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { + if (n == 1) { + orders[orders.length] = order.slice(); // append copy + } else { + for (var i = 0; i < n; i++) { + HeapPermute(n - 1, order, orders); + if (n % 2 == 1) { // swap 0, n-1 + var tmp = order[0]; + order[0] = order[n - 1]; + order[n - 1] = tmp; + } else { // swap i, n-1 + var tmp = order[i]; + order[i] = order[n - 1]; + order[n - 1] = tmp; + } + } + } +} + +function MakeUnistrokes(strokes: any, orders: any) { + var unistrokes = new Array(); // array of point arrays + for (var r = 0; r < orders.length; r++) { + for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions + { + var unistroke = new Array(); // array of points + for (var i = 0; i < orders[r].length; i++) { + var pts; + if (((b >> i) & 1) == 1) {// is b's bit at index i on? + pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse + } + else { + pts = strokes[orders[r][i]].slice(); // copy + } + for (var p = 0; p < pts.length; p++) { + unistroke[unistroke.length] = pts[p]; // append points + } + } + unistrokes[unistrokes.length] = unistroke; // add one unistroke to set + } + } + return unistrokes; +} + +function CombineStrokes(strokes: any) { + var points = new Array(); + for (var s = 0; s < strokes.length; s++) { + for (var p = 0; p < strokes[s].length; p++) + points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y); + } + return points; +} +function Resample(points: any, n: any) { + var I = PathLength(points) / (n - 1); // interval length + var D = 0.0; + var newpoints = new Array(points[0]); + for (var i = 1; i < points.length; i++) { + var d = Distance(points[i - 1], points[i]); + if ((D + d) >= I) { + var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X); + var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y); + var q = new Point(qx, qy); + newpoints[newpoints.length] = q; // append new point 'q' + points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i + D = 0.0; + } + else D += d; + } + if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so + newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y); + return newpoints; +} +function IndicativeAngle(points: any) { + var c = Centroid(points); + return Math.atan2(c.Y - points[0].Y, c.X - points[0].X); +} +function RotateBy(points: any, radians: any) // rotates points around centroid +{ + var c = Centroid(points); + var cos = Math.cos(radians); + var sin = Math.sin(radians); + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X + var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D +{ + var B = BoundingBox(points); + var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); + var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function TranslateTo(points: any, pt: any) // translates points' centroid +{ + var c = Centroid(points); + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = points[i].X + pt.X - c.X; + var qy = points[i].Y + pt.Y - c.Y; + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor +{ + var cos = 1.0; + var sin = 0.0; + if (useBoundedRotationInvariance) { + var iAngle = Math.atan2(points[0].Y, points[0].X); + var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0)); + cos = Math.cos(baseOrientation - iAngle); + sin = Math.sin(baseOrientation - iAngle); + } + var sum = 0.0; + var vector = new Array(); + for (var i = 0; i < points.length; i++) { + var newX = points[i].X * cos - points[i].Y * sin; + var newY = points[i].Y * cos + points[i].X * sin; + vector[vector.length] = newX; + vector[vector.length] = newY; + sum += newX * newX + newY * newY; + } + var magnitude = Math.sqrt(sum); + for (var i = 0; i < vector.length; i++) { + vector[i] /= magnitude; + } + return vector; +} +function OptimalCosineDistance(v1: any, v2: any) // for Protractor +{ + var a = 0.0; + var b = 0.0; + for (var i = 0; i < v1.length; i += 2) { + a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1]; + b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i]; + } + var angle = Math.atan(b / a); + return Math.acos(a * Math.cos(angle) + b * Math.sin(angle)); +} +function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) { + var x1 = Phi * a + (1.0 - Phi) * b; + var f1 = DistanceAtAngle(points, T, x1); + var x2 = (1.0 - Phi) * a + Phi * b; + var f2 = DistanceAtAngle(points, T, x2); + while (Math.abs(b - a) > threshold) { + if (f1 < f2) { + b = x2; + x2 = x1; + f2 = f1; + x1 = Phi * a + (1.0 - Phi) * b; + f1 = DistanceAtAngle(points, T, x1); + } else { + a = x1; + x1 = x2; + f1 = f2; + x2 = (1.0 - Phi) * a + Phi * b; + f2 = DistanceAtAngle(points, T, x2); + } + } + return Math.min(f1, f2); +} +function DistanceAtAngle(points: any, T: any, radians: any) { + var newpoints = RotateBy(points, radians); + return PathDistance(newpoints, T.Points); +} +function Centroid(points: any) { + var x = 0.0, y = 0.0; + for (var i = 0; i < points.length; i++) { + x += points[i].X; + y += points[i].Y; + } + x /= points.length; + y /= points.length; + return new Point(x, y); +} +function BoundingBox(points: any) { + var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity; + for (var i = 0; i < points.length; i++) { + minX = Math.min(minX, points[i].X); + minY = Math.min(minY, points[i].Y); + maxX = Math.max(maxX, points[i].X); + maxY = Math.max(maxY, points[i].Y); + } + return new Rectangle(minX, minY, maxX - minX, maxY - minY); +} +function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths +{ + var d = 0.0; + for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length + d += Distance(pts1[i], pts2[i]); + return d / pts1.length; +} +function PathLength(points: any) // length traversed by a point path +{ + var d = 0.0; + for (var i = 1; i < points.length; i++) + d += Distance(points[i - 1], points[i]); + return d; +} +function Distance(p1: any, p2: any) // distance between two points +{ + var dx = p2.X - p1.X; + var dy = p2.Y - p1.Y; + return Math.sqrt(dx * dx + dy * dy); +} +function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector +{ + var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y); + var len = Math.sqrt(v.X * v.X + v.Y * v.Y); + return new Point(v.X / len, v.Y / len); +} +function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2 +{ + var n = (v1.X * v2.X + v1.Y * v2.Y); + var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1] + return Math.acos(c); // arc cosine of the vector dot product +} +function Deg2Rad(d: any) { return (d * Math.PI / 180.0); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From b3b81c8400ce2a92df1ae8c0db9b422cf4a2cf4c Mon Sep 17 00:00:00 2001 From: yipstanley Date: Sat, 7 Dec 2019 14:48:43 -0500 Subject: i think this is better now --- src/client/util/InteractionUtils.ts | 2 +- src/client/views/Touchable.tsx | 33 ++++++++++++++------------------- src/pen-gestures/GestureUtils.ts | 8 ++++---- 3 files changed, 19 insertions(+), 24 deletions(-) (limited to 'src/pen-gestures') diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts index c24c70bca..7390890c6 100644 --- a/src/client/util/InteractionUtils.ts +++ b/src/client/util/InteractionUtils.ts @@ -53,7 +53,7 @@ export namespace InteractionUtils { * @param oldPoint2 - previous point 2 */ export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { - let threshold = 4; + let threshold = 3; let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2); let newDist = TwoPointEuclidist(pt1, pt2); diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index d0e3db8a5..5dcfec768 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -26,7 +26,8 @@ export abstract class Touchable extends React.Component { } if (this.prevPoints.size) { - switch (e.targetTouches.length) { + console.log(e.targetTouches.length); + switch (this.prevPoints.size) { case 1: this.handle1PointerDown(e); break; @@ -67,35 +68,29 @@ export abstract class Touchable extends React.Component { if (this.prevPoints.has(pt.identifier)) { this.prevPoints.set(pt.identifier, pt); } - else { - this.prevPoints.set(pt.identifier, pt); - } } } } @action protected onTouchEnd = (e: TouchEvent): void => { - console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); - this._touchDrag = false; - e.stopPropagation(); - + // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); // remove all the touches associated with the event - for (let i = 0; i < e.targetTouches.length; i++) { - let pt = e.targetTouches.item(i); - if (pt) { - if (this.prevPoints.has(pt.identifier)) { - console.log("delete"); - this.prevPoints.delete(pt.identifier); - } + for (let i = 0; i < e.changedTouches.length; i++) { + let pt = e.changedTouches.item(i); + if (pt && this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); } } + this._touchDrag = false; + e.stopPropagation(); - if (e.targetTouches.length === 0) { - this.prevPoints.clear(); - } - if (this.prevPoints.size === 0 && e.targetTouches.length === 0) { + // if (e.targetTouches.length === 0) { + // this.prevPoints.clear(); + // } + + if (this.prevPoints.size === 0) { this.cleanUpInteractions(); } } diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index caaa0736d..7a14452c8 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -6,15 +6,15 @@ import { Doc } from "../new_fields/Doc"; export namespace GestureUtils { namespace GestureDataTypes { - export type BoxData = Doc[]; + export type BoxData = Array; } - export const GestureRecognizer = new NDollarRecognizer(false); - export enum Gestures { Box = "box" } + export const GestureRecognizer = new NDollarRecognizer(false); + export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] { switch (name) { case Gestures.Box: @@ -27,7 +27,7 @@ export namespace GestureUtils { } function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] { - if (gestureData instanceof Doc[]) { + if (gestureData instanceof Array) { return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})]; } return []; -- cgit v1.2.3-70-g09d2 From 766e47cd144be6f0bcbb5787dbd174aa1bba3690 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Sat, 7 Dec 2019 16:53:43 -0500 Subject: box !!!! --- src/client/util/InteractionUtils.ts | 2 +- src/client/views/Touchable.tsx | 4 ++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 23 ++++++++++++++++++---- src/client/views/nodes/DocumentView.tsx | 8 ++++---- src/pen-gestures/GestureUtils.ts | 16 ++++----------- src/pen-gestures/ndollar.ts | 2 +- 6 files changed, 31 insertions(+), 24 deletions(-) (limited to 'src/pen-gestures') diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts index 7390890c6..7c386abc2 100644 --- a/src/client/util/InteractionUtils.ts +++ b/src/client/util/InteractionUtils.ts @@ -53,7 +53,7 @@ export namespace InteractionUtils { * @param oldPoint2 - previous point 2 */ export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { - let threshold = 3; + let threshold = window.devicePixelRatio; let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2); let newDist = TwoPointEuclidist(pt1, pt2); diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 5dcfec768..a647dec04 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -18,9 +18,9 @@ export abstract class Touchable extends React.Component { protected onTouchStart = (e: React.TouchEvent): void => { for (let i = 0; i < e.targetTouches.length; i++) { let pt: any = e.targetTouches.item(i); - // pen is also a touch, but with a radius of 0.5 (at least with the surface pens). i doubt anyone's fingers are 2 pixels wide, + // pen is also a touch, but with a radius of 0.5 (at least with the surface pens). // and this seems to be the only way of differentiating pen and touch on touch events - if (pt.radiusX > 2 && pt.radiusY > 2) { + if (pt.radiusX > 0.5 && pt.radiusY > 0.5) { this.prevPoints.set(pt.identifier, pt); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6dd472c84..bec0ace5a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -340,8 +340,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); let result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); - if (result) { - console.log(result.Name); + if (result && result.Score > 0.75) { + switch (result.Name) { + case GestureUtils.Gestures.Box: + let bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.y)), b: Math.max(...this._points.map(p => p.Y)) }; + let sel = this.getActiveDocuments().filter(doc => { + let l = NumCast(doc.x); + let r = l + doc[WidthSym](); + let t = NumCast(doc.y); + let b = t + doc[HeightSym](); + doc.x = l - B.left - B.width / 2; + doc.y = t - B.top - B.height / 2; + return !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); + }); + this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 })); + sel.forEach(d => this.props.removeDocument(d)); + break; + } this._points = []; } else { @@ -474,8 +489,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let rawDelta = (dir * (d1 + d2)); // this floors and ceils the delta value to prevent jitteriness - let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16); - this.zoom(centerX, centerY, delta); + let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8); + this.zoom(centerX, centerY, delta * window.devicePixelRatio); this.prevPoints.set(pt1.identifier, pt1); this.prevPoints.set(pt2.identifier, pt2); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 38c46b258..dfeadb8c0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -142,7 +142,7 @@ export class DocumentView extends DocComponent(Docu (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); let preventDefault = true; - if (this._doubleTap && this.props.renderDepth && !this.onClickHandler ?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click let fullScreenAlias = Doc.MakeAlias(this.props.Document); if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) { fullScreenAlias.layoutKey = "layoutCustom"; @@ -362,7 +362,7 @@ export class DocumentView extends DocComponent(Docu @undoBatch @action setCustomView = (custom: boolean): void => { - if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) { + if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); } else { custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); @@ -687,10 +687,10 @@ export class DocumentView extends DocComponent(Docu return
{ - console.log("Brush" + this.props.Document.title); + // console.log("Brush" + this.props.Document.title); Doc.BrushDoc(this.props.Document); }} onPointerLeave={e => { - console.log("UnBrush" + this.props.Document.title); + // console.log("UnBrush" + this.props.Document.title); Doc.UnBrushDoc(this.props.Document); }} diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index 7a14452c8..955dad5b4 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -2,7 +2,9 @@ import { NDollarRecognizer } from "./ndollar"; import { Type } from "typescript"; import { InkField } from "../new_fields/InkField"; import { Docs } from "../client/documents/Documents"; -import { Doc } from "../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../new_fields/Doc"; +import { NumCast } from "../new_fields/Types"; +import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView"; export namespace GestureUtils { namespace GestureDataTypes { @@ -15,21 +17,11 @@ export namespace GestureUtils { export const GestureRecognizer = new NDollarRecognizer(false); - export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] { + export function GestureOptions(name: string, gestureData?: any): (params: {}) => any { switch (name) { case Gestures.Box: - if (gestureData as GestureDataTypes.BoxData) { - return BoxOptions(gestureData as GestureDataTypes.BoxData); - } break; } throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts"); } - - function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] { - if (gestureData instanceof Array) { - return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})]; - } - return []; - } } \ No newline at end of file diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 5d4384202..8c8e079a4 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -168,7 +168,7 @@ export class NDollarRecognizer { // this.Multistrokes = new Array(NumMultistrokes); this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( - new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146)) + new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146)) )); // -- cgit v1.2.3-70-g09d2 From 3a2aaf5ffec2a568bb749c6ac0df8367a150af39 Mon Sep 17 00:00:00 2001 From: yipstanley Date: Sun, 8 Dec 2019 14:46:46 -0500 Subject: links! --- .../collectionFreeForm/CollectionFreeFormView.tsx | 40 +++++++++++++++++++--- src/pen-gestures/GestureUtils.ts | 3 +- src/pen-gestures/ndollar.ts | 8 ++--- 3 files changed, 41 insertions(+), 10 deletions(-) (limited to 'src/pen-gestures') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bec0ace5a..5a7644d05 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -13,7 +13,7 @@ import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fiel import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; -import { Docs } from "../../../documents/Documents"; +import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; @@ -42,6 +42,7 @@ import React = require("react"); import { computedFn, keepAlive } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; +import { LinkManager } from "../../../util/LinkManager"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -340,7 +341,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); let result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); - if (result && result.Score > 0.75) { + let actionPerformed = false; + if (result && result.Score > 0.7) { switch (result.Name) { case GestureUtils.Gestures.Box: let bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.y)), b: Math.max(...this._points.map(p => p.Y)) }; @@ -355,11 +357,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 })); sel.forEach(d => this.props.removeDocument(d)); + actionPerformed = true; + break; + case GestureUtils.Gestures.Line: + let ep1 = this._points[0]; + let ep2 = this._points[this._points.length - 1]; + let d1: Doc | undefined; + let d2: Doc | undefined; + this.getActiveDocuments().map(doc => { + let l = NumCast(doc.x); + let r = l + doc[WidthSym](); + let t = NumCast(doc.y); + let b = t + doc[HeightSym](); + if (!d1 && l < ep1.X && r > ep1.X && t < ep1.Y && b > ep1.Y) { + d1 = doc; + } + else if (!d2 && l < ep2.X && r > ep2.X && t < ep2.Y && b > ep2.Y) { + d2 = doc; + } + }); + if (d1 && d2) { + if (!LinkManager.Instance.doesLinkExist(d1, d2)) { + DocUtils.MakeLink({ doc: d1 }, { doc: d2 }); + actionPerformed = true; + } + } break; } - this._points = []; + if (actionPerformed) { + this._points = []; + } } - else { + + if (!actionPerformed) { let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); this.addDocument(inkDoc); this._points = []; @@ -880,7 +910,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let B = this.svgBounds; return ( - + {CreatePolyline(this._points, B.left, B.top)} ); diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index 955dad5b4..59a85b66b 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -12,7 +12,8 @@ export namespace GestureUtils { } export enum Gestures { - Box = "box" + Box = "box", + Line = "line" } export const GestureRecognizer = new NDollarRecognizer(false); diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 8c8e079a4..12c2b25bb 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -142,7 +142,7 @@ export class Result { // // NDollarRecognizer constants // -const NumMultistrokes = 1; +const NumMultistrokes = 2; const NumPoints = 96; const SquareSize = 250.0; const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) @@ -170,6 +170,9 @@ export class NDollarRecognizer { this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146)) )); + this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array( + new Array(new Point(12, 347), new Point(119, 347)) + )); // // PREDEFINED STROKES @@ -210,9 +213,6 @@ export class NDollarRecognizer { // new Array(new Point(526, 142), new Point(526, 204)), // new Array(new Point(526, 221)) // )); - // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( - // new Array(new Point(12, 347), new Point(119, 347)) - // )); // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( // new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) // )); -- cgit v1.2.3-70-g09d2 From cef6852d597ce67637466afb36c3498dc84211f6 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Wed, 8 Jan 2020 19:48:28 -0500 Subject: gestures are now overlayed and can span collections/panes! --- src/client/documents/Documents.ts | 3 +- src/client/views/CollectionLinearView.tsx | 4 +- src/client/views/GestureOverlay.scss | 8 + src/client/views/GestureOverlay.tsx | 159 +++++++++++++++++++ src/client/views/InkSelectDecorations.scss | 5 - src/client/views/InkSelectDecorations.tsx | 55 ------- src/client/views/InkingStroke.tsx | 6 +- src/client/views/MainView.scss | 7 +- src/client/views/MainView.tsx | 7 +- .../views/collections/CollectionStackingView.tsx | 6 +- src/client/views/collections/CollectionSubView.tsx | 13 +- .../views/collections/CollectionTreeView.scss | 6 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 171 +++++++-------------- src/client/views/nodes/DocumentView.tsx | 19 ++- src/pen-gestures/GestureUtils.ts | 28 +++- 15 files changed, 299 insertions(+), 198 deletions(-) create mode 100644 src/client/views/GestureOverlay.scss create mode 100644 src/client/views/GestureOverlay.tsx delete mode 100644 src/client/views/InkSelectDecorations.scss delete mode 100644 src/client/views/InkSelectDecorations.tsx (limited to 'src/pen-gestures') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e149963b9..1853be529 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -29,7 +29,7 @@ import { listSpec } from "../../new_fields/Schema"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; -import { UndoManager } from "../util/UndoManager"; +import { UndoManager, undoBatch } from "../util/UndoManager"; import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; @@ -730,6 +730,7 @@ export namespace DocUtils { }); } + @undoBatch export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) { const sv = DocumentManager.Instance.getDocumentView(source.doc); if (sv && sv.props.ContainingCollectionDoc === target.doc) return; diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 5ca861f71..5d6a58656 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 createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDropAndGestureTarget = (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
-
+
this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss new file mode 100644 index 000000000..cbc1b6e7d --- /dev/null +++ b/src/client/views/GestureOverlay.scss @@ -0,0 +1,8 @@ +.gestureOverlay-cont { + width: 100vw; + height: 100vw; + position: absolute; + top: 0; + left: 0; + touch-action: none; +} \ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx new file mode 100644 index 000000000..c88e3f7ae --- /dev/null +++ b/src/client/views/GestureOverlay.tsx @@ -0,0 +1,159 @@ +import React = require("react"); +import { Touchable } from "./Touchable"; +import { observer } from "mobx-react"; +import "./GestureOverlay.scss" +import { computed, observable, action } from "mobx"; +import { CreatePolyline } from "./InkingStroke"; +import { GestureUtils } from "../../pen-gestures/GestureUtils"; +import { InteractionUtils } from "../util/InteractionUtils"; +import { InkingControl } from "./InkingControl"; +import { InkTool } from "../../new_fields/InkField"; +import { Doc } from "../../new_fields/Doc"; +import { LinkManager } from "../util/LinkManager"; +import { DocUtils } from "../documents/Documents"; +import { undoBatch } from "../util/UndoManager"; + +@observer +export default class GestureOverlay extends Touchable { + static Instance: GestureOverlay; + + @observable private _points: { X: number, Y: number }[] = []; + + private _d1: Doc | undefined; + + constructor(props: Readonly<{}>) { + super(props); + + GestureOverlay.Instance = this; + } + + @action + onPointerDown = (e: React.PointerEvent) => { + 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(); + + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + } + } + + @action + onPointerMove = (e: PointerEvent) => { + this._points.push({ X: e.clientX, Y: e.clientY }); + e.stopPropagation(); + e.preventDefault(); + } + + @action + onPointerUp = (e: PointerEvent) => { + 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 target = document.elementFromPoint(this._points[0].X, this._points[0].Y); + target?.dispatchEvent(new CustomEvent("dashOnGesture", + { + bubbles: true, + detail: { + points: this._points, + gesture: GestureUtils.Gestures.Box, + bounds: B + } + })); + 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("dashOnGesture", + { + bubbles: true, + detail: { + points: this._points, + gesture: GestureUtils.Gestures.Line, + bounds: B, + callbackFn: callback + } + }) + target1?.dispatchEvent(ge); + target2?.dispatchEvent(ge); + break; + } + if (actionPerformed) { + this._points = []; + } + } + + if (!actionPerformed) { + const target = document.elementFromPoint(this._points[0].X, this._points[0].Y); + target?.dispatchEvent( + new CustomEvent("dashOnGesture", + { + bubbles: true, + detail: { + points: this._points, + gesture: GestureUtils.Gestures.Stroke, + bounds: B + } + } + ) + ) + this._points = []; + } + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + @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 ( + + {CreatePolyline(this._points, B.left, B.top)} + + ); + } + + render() { + return ( +
+ {this.props.children} + {this.currentStroke} +
); + } +} \ No newline at end of file diff --git a/src/client/views/InkSelectDecorations.scss b/src/client/views/InkSelectDecorations.scss deleted file mode 100644 index daff58fd6..000000000 --- a/src/client/views/InkSelectDecorations.scss +++ /dev/null @@ -1,5 +0,0 @@ -.inkSelectDecorations { - position: absolute; - border: black 1px solid; - z-index: 9001; -} \ No newline at end of file diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx deleted file mode 100644 index 3ad50762d..000000000 --- a/src/client/views/InkSelectDecorations.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React = require("react"); -import { Touchable } from "./Touchable"; -import { PointData } from "../../new_fields/InkField"; -import { observer } from "mobx-react"; -import { computed, observable, action, runInAction } from "mobx"; -import "./InkSelectDecorations.scss"; - -@observer -export default class InkSelectDecorations extends Touchable { - static Instance: InkSelectDecorations; - - @observable private _selectedInkNodes: Map = new Map(); - - constructor(props: Readonly<{}>) { - super(props); - - InkSelectDecorations.Instance = this; - } - - @action - public SetSelected = (inkNodes: Map, keepOld: boolean = false) => { - if (!keepOld) { - this._selectedInkNodes = new Map(); - } - inkNodes.forEach((value: any, key: any) => { - runInAction(() => this._selectedInkNodes.set(key, value)); - }); - } - - @computed - get Bounds(): { x: number, y: number, b: number, r: number } { - const left = Number.MAX_VALUE; - const top = Number.MAX_VALUE; - const right = -Number.MAX_VALUE; - const bottom = -Number.MAX_VALUE; - this._selectedInkNodes.forEach((value: PointData, key: string) => { - // value.pathData.map(val => { - // left = Math.min(val.x, left); - // top = Math.min(val.y, top); - // right = Math.max(val.x, right); - // bottom = Math.max(val.y, bottom); - // }); - }); - return { x: left, y: top, b: bottom, r: right }; - } - - render() { - const bounds = this.Bounds; - return
; - } -} \ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index a413eebc9..8b346d5d9 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -9,6 +9,7 @@ import { InkingControl } from "./InkingControl"; import "./InkingStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); +import { TraceMobx } from "../../new_fields/util"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -35,6 +36,7 @@ export class InkingStroke extends DocExtendableComponent p.X); const ys = data.map(p => p.Y); @@ -42,7 +44,7 @@ export class InkingStroke extends DocExtendableComponent diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 4c8c95529..2a1cc3303 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -9,8 +9,8 @@ .mainContent-div { position: relative; - width:100%; - height:100%; + width: 100%; + height: 100%; } // add nodes menu. Note that the + button is actually an input label, not an actual button. @@ -28,6 +28,7 @@ top: 0; left: 0; z-index: 1; + touch-action: none; } .mainView-mainContent { @@ -66,7 +67,7 @@ .mainView-libraryFlyout { height: 100%; - width:100%; + width: 100%; position: absolute; display: flex; flex-direction: column; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index a1196ee1c..14c1803c0 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -36,7 +36,7 @@ import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; -import InkSelectDecorations from './InkSelectDecorations'; +import GestureOverlay from './GestureOverlay'; import { Scripting } from '../util/Scripting'; import { AudioBox } from './nodes/AudioBox'; import { TraceMobx } from '../../new_fields/util'; @@ -510,8 +510,9 @@ export class MainView extends React.Component { - - {this.mainContent} + + {this.mainContent} + diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index e71e11b48..992820fc7 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.createDropTarget(ele!); //so the whole grid is the drop target? + this.createDropAndGestureTarget(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.createDropTarget} + createDropTarget={this.createDropAndGestureTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} />; } @@ -342,7 +342,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} + createDropTarget={this.createDropAndGestureTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} setDocHeight={this.setDocHeight} />; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 062521690..73dc7edc6 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -23,6 +23,7 @@ import { basename } from 'path'; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; +import { GestureUtils } from "../../../pen-gestures/GestureUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -47,15 +48,18 @@ export interface SubCollectionViewProps extends CollectionViewProps { export function CollectionSubView(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; + private gestureDisposer?: GestureUtils.GestureEventDisposer; private _childLayoutDisposer?: IReactionDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer && this.dropDisposer(); + this.gestureDisposer && this.gestureDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view - this.createDropTarget(ele); + this.createDropAndGestureTarget(ele); } componentDidMount() { @@ -129,6 +133,11 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } } + @undoBatch + protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { + + } + @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 0b9dc2eb2..2fa6813d7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -7,9 +7,9 @@ border-radius: inherit; box-sizing: border-box; height: 100%; - width:100%; + width: 100%; position: relative; - top:0; + top: 0; padding-left: 10px; padding-right: 10px; background: $light-color-secondary; @@ -17,6 +17,7 @@ overflow: auto; user-select: none; cursor: default; + touch-action: pan-y; ul { list-style: none; @@ -115,6 +116,7 @@ .treeViewItem-header { border: transparent 1px solid; display: flex; + .editableView-container-editing-oneLine { min-width: 15px; } 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 ( - - {CreatePolyline(this._points, B.left, B.top)} - - ); - } - 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(); 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
{!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) ? diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a14f69f71..b544319d7 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -45,6 +45,7 @@ import { InkTool } from '../../../new_fields/InkField'; import { TraceMobx } from '../../../new_fields/util'; import { List } from '../../../new_fields/List'; import { FormattedTextBoxComment } from './FormattedTextBoxComment'; +import { GestureUtils } from '../../../pen-gestures/GestureUtils'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -94,6 +95,7 @@ export class DocumentView extends DocComponent(Docu private _hitTemplateDrag = false; private _mainCont = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; + private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _titleRef = React.createRef(); public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive @@ -107,6 +109,7 @@ export class DocumentView extends DocComponent(Docu @action 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.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this); } @@ -365,10 +368,11 @@ export class DocumentView extends DocComponent(Docu // console.log(e.button) // console.log(e.nativeEvent) // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + return; + } if (!InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE)) { - if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - e.stopPropagation(); - } + e.stopPropagation(); return; } if ((!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart)) { @@ -424,6 +428,15 @@ export class DocumentView extends DocComponent(Docu this._lastTap = Date.now(); } + onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { + switch (ge.gesture) { + case GestureUtils.Gestures.Line: + ge.callbackFn && ge.callbackFn(this.props.Document); + e.stopPropagation(); + break; + } + } + @undoBatch deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index 59a85b66b..4edcfa623 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -1,19 +1,43 @@ import { NDollarRecognizer } from "./ndollar"; import { Type } from "typescript"; -import { InkField } from "../new_fields/InkField"; +import { InkField, PointData } from "../new_fields/InkField"; import { Docs } from "../client/documents/Documents"; import { Doc, WidthSym, HeightSym } from "../new_fields/Doc"; import { NumCast } from "../new_fields/Types"; import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView"; +import { Rect } from "react-measure"; export namespace GestureUtils { namespace GestureDataTypes { export type BoxData = Array; } + export class GestureEvent { + constructor( + readonly gesture: Gestures, + readonly points: PointData[], + readonly bounds: Rect, + readonly callbackFn?: Function + ) { } + } + + export interface GestureEventDisposer { (): void; } + + export function MakeGestureTarget( + element: HTMLElement, + func: (e: Event, ge: GestureEvent) => void + ): GestureEventDisposer { + const handler = (e: Event) => func(e, (e as CustomEvent).detail); + element.addEventListener("dashOnGesture", handler); + return () => { + element.removeEventListener("dashOnGesture", handler); + } + } + export enum Gestures { Box = "box", - Line = "line" + Line = "line", + Stroke = "stroke" } export const GestureRecognizer = new NDollarRecognizer(false); -- cgit v1.2.3-70-g09d2 From 540bda7295f6ee7c2eed848598de6f5df74b2723 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 9 Jan 2020 10:04:24 -0500 Subject: fixed many warnings --- src/client/util/TooltipTextMenu.tsx | 4 +- .../CollectionFreeFormLinkView.tsx | 12 +- src/client/views/nodes/DocumentView.tsx | 1 - src/pen-gestures/ndollar.ts | 122 +++++++++++---------- 4 files changed, 71 insertions(+), 68 deletions(-) (limited to 'src/pen-gestures') diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 8aa304fad..e56930624 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -80,7 +80,7 @@ export class TooltipTextMenu { span.appendChild(svg); return span; - } + }; const basicItems = [ // init basicItems in minimized toolbar -- paths to svgs are obtained from fontawesome { mark: schema.marks.strong, dom: svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") }, @@ -883,7 +883,7 @@ export class TooltipTextMenu { if (!lastState || !lastState.doc.eq(view.state.doc) || !lastState.selection.eq(view.state.selection)) { // UPDATE LINK DROPDOWN - const linkTarget = await this.getTextLinkTargetTitle() + const linkTarget = await this.getTextLinkTargetTitle(); const linkDom = this.createLinkTool(linkTarget ? true : false).render(this.view).dom; const linkDropdownDom = this.createLinkDropdown(linkTarget).render(this.view).dom; this.linkDom && this.tooltip.replaceChild(linkDom, this.linkDom); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 178a5bcdc..b8fbaef5c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -54,8 +54,8 @@ export class CollectionFreeFormLinkView extends React.Component { (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); - let m = targetBhyperlink.getBoundingClientRect(); - let mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetBhyperlink.getBoundingClientRect(); + const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; }, 0); @@ -66,8 +66,8 @@ export class CollectionFreeFormLinkView extends React.Component { (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); - let m = targetAhyperlink.getBoundingClientRect(); - let mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetAhyperlink.getBoundingClientRect(); + const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; }, 0); @@ -93,8 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component(Docu document.removeEventListener("touchend", this.onTouchEnd); document.addEventListener("touchend", this.onTouchEnd); if ((e.nativeEvent as any).formattedHandled) e.stopPropagation(); - console.log("down") } } diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 12c2b25bb..872c524d6 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -95,7 +95,7 @@ export class Unistroke { constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) { this.Points = Resample(points, NumPoints); - var radians = IndicativeAngle(this.Points); + const radians = IndicativeAngle(this.Points); this.Points = RotateBy(this.Points, -radians); this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold); if (useBoundedRotationInvariance) { @@ -117,14 +117,14 @@ export class Multistroke { { this.NumStrokes = strokes.length; // number of individual strokes - var order = new Array(strokes.length); // array of integer indices + const order = new Array(strokes.length); // array of integer indices for (var i = 0; i < strokes.length; i++) { order[i] = i; // initialize } - var orders = new Array(); // array of integer arrays + const orders = new Array(); // array of integer arrays HeapPermute(strokes.length, order, /*out*/ orders); - var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays + const unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke for (var j = 0; j < unistrokes.length; j++) { this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]); @@ -247,15 +247,15 @@ export class NDollarRecognizer { } Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => { - var t0 = Date.now(); - var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes - var candidate = new Unistroke("", useBoundedRotationInvariance, points); + const t0 = Date.now(); + const points = CombineStrokes(strokes); // make one connected unistroke from the given strokes + const candidate = new Unistroke("", useBoundedRotationInvariance, points); var u = -1; var b = +Infinity; for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template { - if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes + if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes { for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke { @@ -276,15 +276,15 @@ export class NDollarRecognizer { } } } - var t1 = Date.now(); - return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); + const t1 = Date.now(); + return (u === -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); } AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes); var num = 0; for (var i = 0; i < this.Multistrokes.length; i++) { - if (this.Multistrokes[i].Name == name) { + if (this.Multistrokes[i].Name === name) { num++; } } @@ -302,17 +302,17 @@ export class NDollarRecognizer { // Private helper functions from here on down // function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { - if (n == 1) { + if (n === 1) { orders[orders.length] = order.slice(); // append copy } else { for (var i = 0; i < n; i++) { HeapPermute(n - 1, order, orders); - if (n % 2 == 1) { // swap 0, n-1 - var tmp = order[0]; + if (n % 2 === 1) { // swap 0, n-1 + const tmp = order[0]; order[0] = order[n - 1]; order[n - 1] = tmp; } else { // swap i, n-1 - var tmp = order[i]; + const tmp = order[i]; order[i] = order[n - 1]; order[n - 1] = tmp; } @@ -321,14 +321,14 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { } function MakeUnistrokes(strokes: any, orders: any) { - var unistrokes = new Array(); // array of point arrays + const unistrokes = new Array(); // array of point arrays for (var r = 0; r < orders.length; r++) { for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions { - var unistroke = new Array(); // array of points + const unistroke = new Array(); // array of points for (var i = 0; i < orders[r].length; i++) { var pts; - if (((b >> i) & 1) == 1) {// is b's bit at index i on? + if (((b >> i) & 1) === 1) {// is b's bit at index i on? pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse } else { @@ -345,69 +345,71 @@ function MakeUnistrokes(strokes: any, orders: any) { } function CombineStrokes(strokes: any) { - var points = new Array(); + const points = new Array(); for (var s = 0; s < strokes.length; s++) { - for (var p = 0; p < strokes[s].length; p++) + for (var p = 0; p < strokes[s].length; p++) { points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y); + } } return points; } function Resample(points: any, n: any) { - var I = PathLength(points) / (n - 1); // interval length + const I = PathLength(points) / (n - 1); // interval length var D = 0.0; - var newpoints = new Array(points[0]); + const newpoints = new Array(points[0]); for (var i = 1; i < points.length; i++) { - var d = Distance(points[i - 1], points[i]); + const d = Distance(points[i - 1], points[i]); if ((D + d) >= I) { - var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X); - var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y); - var q = new Point(qx, qy); + const qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X); + const qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y); + const q = new Point(qx, qy); newpoints[newpoints.length] = q; // append new point 'q' points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i D = 0.0; } else D += d; } - if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so + if (newpoints.length === n - 1) {// sometimes we fall a rounding-error short of adding the last point, so add it if so newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y); + } return newpoints; } function IndicativeAngle(points: any) { - var c = Centroid(points); + const c = Centroid(points); return Math.atan2(c.Y - points[0].Y, c.X - points[0].X); } function RotateBy(points: any, radians: any) // rotates points around centroid { - var c = Centroid(points); - var cos = Math.cos(radians); - var sin = Math.sin(radians); - var newpoints = new Array(); + const c = Centroid(points); + const cos = Math.cos(radians); + const sin = Math.sin(radians); + const newpoints = new Array(); for (var i = 0; i < points.length; i++) { - var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X - var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; + const qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X; + const qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; } function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D { - var B = BoundingBox(points); - var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test - var newpoints = new Array(); + const B = BoundingBox(points); + const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test + const newpoints = new Array(); for (var i = 0; i < points.length; i++) { - var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); - var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); + const qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); + const qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; } function TranslateTo(points: any, pt: any) // translates points' centroid { - var c = Centroid(points); - var newpoints = new Array(); + const c = Centroid(points); + const newpoints = new Array(); for (var i = 0; i < points.length; i++) { - var qx = points[i].X + pt.X - c.X; - var qy = points[i].Y + pt.Y - c.Y; + const qx = points[i].X + pt.X - c.X; + const qy = points[i].Y + pt.Y - c.Y; newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; @@ -417,21 +419,21 @@ function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protra var cos = 1.0; var sin = 0.0; if (useBoundedRotationInvariance) { - var iAngle = Math.atan2(points[0].Y, points[0].X); - var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0)); + const iAngle = Math.atan2(points[0].Y, points[0].X); + const baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0)); cos = Math.cos(baseOrientation - iAngle); sin = Math.sin(baseOrientation - iAngle); } var sum = 0.0; - var vector = new Array(); + const vector = new Array(); for (var i = 0; i < points.length; i++) { - var newX = points[i].X * cos - points[i].Y * sin; - var newY = points[i].Y * cos + points[i].X * sin; + const newX = points[i].X * cos - points[i].Y * sin; + const newY = points[i].Y * cos + points[i].X * sin; vector[vector.length] = newX; vector[vector.length] = newY; sum += newX * newX + newY * newY; } - var magnitude = Math.sqrt(sum); + const magnitude = Math.sqrt(sum); for (var i = 0; i < vector.length; i++) { vector[i] /= magnitude; } @@ -445,7 +447,7 @@ function OptimalCosineDistance(v1: any, v2: any) // for Protractor a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1]; b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i]; } - var angle = Math.atan(b / a); + const angle = Math.atan(b / a); return Math.acos(a * Math.cos(angle) + b * Math.sin(angle)); } function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) { @@ -471,7 +473,7 @@ function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any return Math.min(f1, f2); } function DistanceAtAngle(points: any, T: any, radians: any) { - var newpoints = RotateBy(points, radians); + const newpoints = RotateBy(points, radians); return PathDistance(newpoints, T.Points); } function Centroid(points: any) { @@ -497,33 +499,35 @@ function BoundingBox(points: any) { function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths { var d = 0.0; - for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length + for (var i = 0; i < pts1.length; i++) {// assumes pts1.length == pts2.length d += Distance(pts1[i], pts2[i]); + } return d / pts1.length; } function PathLength(points: any) // length traversed by a point path { var d = 0.0; - for (var i = 1; i < points.length; i++) + for (var i = 1; i < points.length; i++) { d += Distance(points[i - 1], points[i]); + } return d; } function Distance(p1: any, p2: any) // distance between two points { - var dx = p2.X - p1.X; - var dy = p2.Y - p1.Y; + const dx = p2.X - p1.X; + const dy = p2.Y - p1.Y; return Math.sqrt(dx * dx + dy * dy); } function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector { - var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y); - var len = Math.sqrt(v.X * v.X + v.Y * v.Y); + const v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y); + const len = Math.sqrt(v.X * v.X + v.Y * v.Y); return new Point(v.X / len, v.Y / len); } function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2 { - var n = (v1.X * v2.X + v1.Y * v2.Y); - var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1] + const n = (v1.X * v2.X + v1.Y * v2.Y); + const c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1] return Math.acos(c); // arc cosine of the vector dot product } function Deg2Rad(d: any) { return (d * Math.PI / 180.0); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 36eed6cc3a3387a1f7755a848aea4e19e2645e14 Mon Sep 17 00:00:00 2001 From: Stanley Yip 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/pen-gestures') 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("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("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 { + render() { + return ( +
+
+
+
console.log("hi")}>1
+
2
+
3
+
+
+
+ ); + } +} \ 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 extends React.Component { 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 extends React.Component { 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 extends React.Component { } } 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 extends React.Component { 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 = ; + + 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(); 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(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(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(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(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(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(( - -), - 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(( + // + + ), + 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 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}> + ; + } + return "hello"; + } + + render() { + return ( +
+ {this.mainContent} +
+ ); + } +} \ 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(), { 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(["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 6162c951e07874fbb12717d4bcd2cd649e41d0d2 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 13 Jan 2020 00:00:56 -0500 Subject: deleted old Session folder and fixed linter errors --- src/client/util/ProseMirrorEditorView.tsx | 8 +- src/client/util/RichTextMenu.tsx | 13 +- src/client/views/EditableView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 4 +- src/client/views/linking/LinkMenuGroup.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 23 +- src/pen-gestures/ndollar.ts | 66 +- src/server/MemoryDatabase.ts | 7 +- src/server/RouteManager.ts | 3 +- src/server/Session/session.ts | 686 --------------------- 11 files changed, 67 insertions(+), 749 deletions(-) delete mode 100644 src/server/Session/session.ts (limited to 'src/pen-gestures') diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx index 3e5fd0604..b42adfbb4 100644 --- a/src/client/util/ProseMirrorEditorView.tsx +++ b/src/client/util/ProseMirrorEditorView.tsx @@ -18,23 +18,23 @@ export class ProseMirrorEditorView extends React.Component { - if (element != null) { + if (element !== null) { this._editorView = new EditorView(element, { state: this.props.editorState, dispatchTransaction: this.dispatchTransaction, }); } - }; + } dispatchTransaction = (tx: any) => { // In case EditorView makes any modification to a state we funnel those // modifications up to the parent and apply to the EditorView itself. const editorState = this.props.editorState.apply(tx); - if (this._editorView != null) { + if (this._editorView) { this._editorView.updateState(editorState); } this.props.onEditorState(editorState); - }; + } focus() { if (this._editorView) { diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx index 639772faa..419d7caf9 100644 --- a/src/client/util/RichTextMenu.tsx +++ b/src/client/util/RichTextMenu.tsx @@ -175,7 +175,7 @@ export default class RichTextMenu extends AntimodeMenu { setMark = (mark: Mark, state: EditorState, dispatch: any) => { if (mark) { const node = (state.selection as NodeSelection).node; - if (node ?.type === schema.nodes.ordered_list) { + if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family }; if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize }; @@ -294,8 +294,8 @@ export default class RichTextMenu extends AntimodeMenu { e.preventDefault(); e.stopPropagation(); self.view && self.view.focus(); - self.view && command && command(self.view!.state, self.view!.dispatch, self.view); - self.view && onclick && onclick(self.view!.state, self.view!.dispatch, self.view); + self.view && command && command(self.view.state, self.view.dispatch, self.view); + self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); self.setActiveMarkButtons(self.getActiveMarksOnSelection()); } @@ -602,7 +602,7 @@ export default class RichTextMenu extends AntimodeMenu { const link = this.currentLink ? this.currentLink : ""; - const button = + const button = ; const dropdownContent =
@@ -684,8 +684,9 @@ export default class RichTextMenu extends AntimodeMenu { } } else { if (node) { - const extension = this.linkExtend(this.view!.state.selection.$anchor, href); - this.view!.dispatch(this.view!.state.tr.removeMark(extension.from, extension.to, this.view!.state.schema.marks.link)); + const { tr, schema, selection } = this.view.state; + const extension = this.linkExtend(selection.$anchor, href); + this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link)); } } } diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 54def38b5..faf02b946 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -36,7 +36,7 @@ export interface EditableProps { resetValue: () => void; value: string, onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void, - autosuggestProps: Autosuggest.AutosuggestProps + autosuggestProps: Autosuggest.AutosuggestProps }; oneLine?: boolean; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 936c4413f..e3780261d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -988,8 +988,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ; } @computed get contentScaling() { - let hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; - let wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; + const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; + const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; return wscale < hscale ? wscale : hscale; } render() { diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index abd17ec4d..ace9a9e4c 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component { document.removeEventListener("pointerup", this.onLinkButtonUp); const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[]; - DragManager.StartLinkTargetsDrag(this._drag.current!, e.x, e.y, this.props.sourceDoc, targets); + DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets); } e.stopPropagation(); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 10d2e2b3e..ba35366ff 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -196,7 +196,7 @@ export class DocumentView extends DocComponent(Docu ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); } else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script FormattedTextBoxComment.Hide(); - this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0]!, this.props.Document)])); + this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)])); } else if (this.Document.isButton) { SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. this.buttonClick(e.altKey, e.ctrlKey); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8e28cf928..8b5c24878 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -906,15 +906,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this.tryUpdateHeight(); // see if we need to preserve the insertion point - const prosediv = this.ProseRef ?.children ?.[0] as any; - const keeplocation = prosediv ?.keeplocation; + const prosediv = this.ProseRef?.children?.[0] as any; + const keeplocation = prosediv?.keeplocation; prosediv && (prosediv.keeplocation = undefined); - const pos = this._editorView ?.state.selection.$from.pos || 1; - keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + const pos = this._editorView?.state.selection.$from.pos || 1; + keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); // jump rich text menu to this textbox - if (this._ref.current) { - const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); + const { current } = this._ref; + if (current) { + const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50; RichTextMenu.Instance.jumpTo(x, y); } @@ -933,7 +934,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) - if (pcords && node ?.type === this._editorView!.state.schema.nodes.dashComment) { + if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2))); e.preventDefault(); } @@ -996,7 +997,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & for (let off = 1; off < 100; off++) { const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); - if (node ?.type === schema.nodes.list_item) { + if (node?.type === schema.nodes.list_item) { list_node = node; break; } @@ -1087,7 +1088,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } if (e.key === "Escape") { this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur ?.(); + (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); } e.stopPropagation(); @@ -1109,7 +1110,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & @action tryUpdateHeight(limitHeight?: number) { - let scrollHeight = this._ref.current ?.scrollHeight; + let scrollHeight = this._ref.current?.scrollHeight; if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation if (limitHeight && scrollHeight > limitHeight) { @@ -1171,7 +1172,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
this.toggleSidebar()} /> :
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}> this.sidebarWidth} diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 872c524d6..ef5ca38c6 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -257,16 +257,16 @@ export class NDollarRecognizer { { if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes { - for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke + for (const unistroke of this.Multistrokes[i].Unistrokes) // for each unistroke within this multistroke { - if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction + if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction { var d; if (useProtractor) { - d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor + d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor } else { - d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N) + d = DistanceAtBestAngle(candidate.Points, unistroke, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N) } if (d < b) { b = d; // best (least) distance @@ -283,8 +283,8 @@ export class NDollarRecognizer { AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes); var num = 0; - for (var i = 0; i < this.Multistrokes.length; i++) { - if (this.Multistrokes[i].Name === name) { + for (const multistroke of this.Multistrokes) { + if (multistroke.Name === name) { num++; } } @@ -322,20 +322,20 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { function MakeUnistrokes(strokes: any, orders: any) { const unistrokes = new Array(); // array of point arrays - for (var r = 0; r < orders.length; r++) { - for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions + for (const order of orders) { + for (var b = 0; b < Math.pow(2, order.length); b++) // use b's bits for directions { const unistroke = new Array(); // array of points - for (var i = 0; i < orders[r].length; i++) { + for (var i = 0; i < order.length; i++) { var pts; if (((b >> i) & 1) === 1) {// is b's bit at index i on? - pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse + pts = strokes[order[i]].slice().reverse(); // copy and reverse } else { - pts = strokes[orders[r][i]].slice(); // copy + pts = strokes[order[i]].slice(); // copy } - for (var p = 0; p < pts.length; p++) { - unistroke[unistroke.length] = pts[p]; // append points + for (const point of pts) { + unistroke[unistroke.length] = point; // append points } } unistrokes[unistrokes.length] = unistroke; // add one unistroke to set @@ -346,9 +346,9 @@ function MakeUnistrokes(strokes: any, orders: any) { function CombineStrokes(strokes: any) { const points = new Array(); - for (var s = 0; s < strokes.length; s++) { - for (var p = 0; p < strokes[s].length; p++) { - points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y); + for (const stroke of strokes) { + for (const { X, Y } of stroke) { + points[points.length] = new Point(X, Y); } } return points; @@ -384,9 +384,9 @@ function RotateBy(points: any, radians: any) // rotates points around centroid const cos = Math.cos(radians); const sin = Math.sin(radians); const newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - const qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X; - const qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; + for (const point of points) { + const qx = (point.X - c.X) * cos - (point.Y - c.Y) * sin + c.X; + const qy = (point.X - c.X) * sin + (point.Y - c.Y) * cos + c.Y; newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; @@ -396,9 +396,9 @@ function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniform const B = BoundingBox(points); const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test const newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - const qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); - const qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); + for (const { X, Y } of points) { + const qx = uniformly ? X * (size / Math.max(B.Width, B.Height)) : X * (size / B.Width); + const qy = uniformly ? Y * (size / Math.max(B.Width, B.Height)) : Y * (size / B.Height); newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; @@ -407,9 +407,9 @@ function TranslateTo(points: any, pt: any) // translates points' centroid { const c = Centroid(points); const newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - const qx = points[i].X + pt.X - c.X; - const qy = points[i].Y + pt.Y - c.Y; + for (const { X, Y } of points) { + const qx = X + pt.X - c.X; + const qy = Y + pt.Y - c.Y; newpoints[newpoints.length] = new Point(qx, qy); } return newpoints; @@ -478,9 +478,9 @@ function DistanceAtAngle(points: any, T: any, radians: any) { } function Centroid(points: any) { var x = 0.0, y = 0.0; - for (var i = 0; i < points.length; i++) { - x += points[i].X; - y += points[i].Y; + for (const point of points) { + x += point.X; + y += point.Y; } x /= points.length; y /= points.length; @@ -488,11 +488,11 @@ function Centroid(points: any) { } function BoundingBox(points: any) { var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity; - for (var i = 0; i < points.length; i++) { - minX = Math.min(minX, points[i].X); - minY = Math.min(minY, points[i].Y); - maxX = Math.max(maxX, points[i].X); - maxY = Math.max(maxY, points[i].Y); + for (const { X, Y } of points) { + minX = Math.min(minX, X); + minY = Math.min(minY, Y); + maxX = Math.max(maxX, X); + maxY = Math.max(maxY, Y); } return new Rectangle(minX, minY, maxX - minX, maxY - minY); } diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts index e7396babf..543f96e7f 100644 --- a/src/server/MemoryDatabase.ts +++ b/src/server/MemoryDatabase.ts @@ -7,7 +7,7 @@ export class MemoryDatabase implements IDatabase { private db: { [collectionName: string]: { [id: string]: any } } = {}; private getCollection(collectionName: string) { - let collection = this.db[collectionName]; + const collection = this.db[collectionName]; if (collection) { return collection; } else { @@ -17,9 +17,10 @@ export class MemoryDatabase implements IDatabase { public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise { const collection = this.getCollection(collectionName); - if ("$set" in value) { + const set = "$set"; + if (set in value) { let currentVal = collection[id] ?? (collection[id] = {}); - const val = value["$set"]; + const val = value[set]; for (const key in val) { const keys = key.split("."); for (let i = 0; i < keys.length - 1; i++) { diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index a7ee405a7..5afd607fd 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -86,7 +86,8 @@ export default class RouteManager { const { method, subscription, secureHandler: onValidation, publicHandler: onUnauthenticated, errorHandler: onError } = initializer; const isRelease = this._isRelease; const supervised = async (req: express.Request, res: express.Response) => { - let { user, originalUrl: target } = req; + let { user } = req; + const { originalUrl: target } = req; if (process.env.DB === "MEM" && !user) { user = { id: "guest", email: "", userDocumentId: "guestDocId" }; } diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts deleted file mode 100644 index ec3d46ac1..000000000 --- a/src/server/Session/session.ts +++ /dev/null @@ -1,686 +0,0 @@ -import { red, cyan, green, yellow, magenta, blue, white, Color, grey, gray, black } from "colors"; -import { on, fork, setupMaster, Worker, isMaster, isWorker } from "cluster"; -import { get } from "request-promise"; -import { Utils } from "../../Utils"; -import Repl, { ReplAction } from "../repl"; -import { readFileSync } from "fs"; -import { validate, ValidationError } from "jsonschema"; -import { configurationSchema } from "./session_config_schema"; -import { exec, ExecOptions } from "child_process"; - -/** - * This namespace relies on NodeJS's cluster module, which allows a parent (master) process to share - * code with its children (workers). A simple `isMaster` flag indicates who is trying to access - * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally). - * - * Think of the master thread as a factory, and the workers as the helpers that actually run the server. - * - * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process - * This will spawn off its own child process (by default, mirrors the execution path of its parent), - * in which initializeWorker() is invoked. - */ -export namespace Session { - - type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; - const colorMapping: Map = new Map([ - ["yellow", yellow], - ["red", red], - ["cyan", cyan], - ["green", green], - ["blue", blue], - ["magenta", magenta], - ["grey", grey], - ["gray", gray], - ["white", white], - ["black", black] - ]); - - export abstract class AppliedSessionAgent { - - // the following two methods allow the developer to create a custom - // session and use the built in customization options for each thread - protected abstract async launchMonitor(): Promise; - protected abstract async launchServerWorker(): Promise; - - private launched = false; - - public killSession = (reason: string, graceful = true, errorCode = 0) => { - const target = isMaster ? this.sessionMonitor : this.serverWorker; - target.killSession(reason, graceful, errorCode); - } - - private sessionMonitorRef: Session.Monitor | undefined; - public get sessionMonitor(): Session.Monitor { - if (!isMaster) { - this.serverWorker.sendMonitorAction("kill", { - graceful: false, - reason: "Cannot access the session monitor directly from the server worker thread.", - errorCode: 1 - }); - throw new Error(); - } - return this.sessionMonitorRef!; - } - - private serverWorkerRef: Session.ServerWorker | undefined; - public get serverWorker(): Session.ServerWorker { - if (isMaster) { - throw new Error("Cannot access the server worker directly from the session monitor thread"); - } - return this.serverWorkerRef!; - } - - public async launch(): Promise { - if (!this.launched) { - this.launched = true; - if (isMaster) { - this.sessionMonitorRef = await this.launchMonitor(); - } else { - this.serverWorkerRef = await this.launchServerWorker(); - } - } else { - throw new Error("Cannot launch a session thread more than once per process."); - } - } - - } - - interface Identifier { - text: string; - color: ColorLabel; - } - - interface Identifiers { - master: Identifier; - worker: Identifier; - exec: Identifier; - } - - interface Configuration { - showServerOutput: boolean; - identifiers: Identifiers; - ports: { [description: string]: number }; - polling: { - route: string; - intervalSeconds: number; - failureTolerance: number; - }; - } - - const defaultConfig: Configuration = { - showServerOutput: false, - identifiers: { - master: { - text: "__monitor__", - color: "yellow" - }, - worker: { - text: "__server__", - color: "magenta" - }, - exec: { - text: "__exec__", - color: "green" - } - }, - ports: { server: 3000 }, - polling: { - route: "/", - intervalSeconds: 30, - failureTolerance: 0 - } - }; - - export type ExitHandler = (reason: Error | boolean) => void | Promise; - - export namespace Monitor { - - export interface NotifierHooks { - key?: (key: string) => (boolean | Promise); - crash?: (error: Error) => (boolean | Promise); - } - - export interface Action { - message: string; - args: any; - } - - export type ServerMessageHandler = (action: Action) => void | Promise; - - } - - /** - * Validates and reads the configuration file, accordingly builds a child process factory - * and spawns off an initial process that will respawn as predecessors die. - */ - export class Monitor { - - private static count = 0; - private exitHandlers: ExitHandler[] = []; - private readonly notifiers: Monitor.NotifierHooks | undefined; - private readonly config: Configuration; - private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {}; - private activeWorker: Worker | undefined; - private key: string | undefined; - private repl: Repl; - - public static Create(notifiers?: Monitor.NotifierHooks) { - if (isWorker) { - process.send?.({ - action: { - message: "kill", - args: { - reason: "cannot create a monitor on the worker process.", - graceful: false, - errorCode: 1 - } - } - }); - process.exit(1); - } else if (++Monitor.count > 1) { - console.error(red("cannot create more than one monitor.")); - process.exit(1); - } else { - return new Monitor(notifiers); - } - } - - /** - * Kill this session and its active child - * server process, either gracefully (may wait - * indefinitely, but at least allows active networking - * requests to complete) or immediately. - */ - public killSession = async (reason: string, graceful = true, errorCode = 0) => { - this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`)); - this.mainLog(`session exit reason: ${(red(reason))}`); - await this.executeExitHandlers(true); - this.killActiveWorker(graceful, true); - process.exit(errorCode); - } - - /** - * Execute the list of functions registered to be called - * whenever the process exits. - */ - public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); - - /** - * Extend the default repl by adding in custom commands - * that can invoke application logic external to this module - */ - public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { - this.repl.registerCommand(basename, argPatterns, action); - } - - public exec = (command: string, options?: ExecOptions) => { - return new Promise(resolve => { - exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => { - if (error) { - this.execLog(red(`unable to execute ${white(command)}`)); - error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`))); - } else { - let outLines: string[], errorLines: string[]; - if ((outLines = stdout.split("\n").filter(line => line.length)).length) { - outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`))); - } - if ((errorLines = stderr.split("\n").filter(line => line.length)).length) { - errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`))); - } - } - resolve(); - }); - }); - } - - /** - * Add a listener at this message. When the monitor process - * receives a message, it will invoke all registered functions. - */ - public addServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => { - const handlers = this.onMessage[message]; - if (handlers) { - handlers.push(handler); - } else { - this.onMessage[message] = [handler]; - } - } - - /** - * Unregister a given listener at this message. - */ - public removeServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => { - const handlers = this.onMessage[message]; - if (handlers) { - const index = handlers.indexOf(handler); - if (index > -1) { - handlers.splice(index, 1); - } - } - } - - /** - * Unregister all listeners at this message. - */ - public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined; - - private constructor(notifiers?: Monitor.NotifierHooks) { - this.notifiers = notifiers; - - console.log(this.timestamp(), cyan("initializing session...")); - - this.config = this.loadAndValidateConfiguration(); - - this.initializeSessionKey(); - // determines whether or not we see the compilation / initialization / runtime output of each child server process - const output = this.config.showServerOutput ? "inherit" : "ignore"; - setupMaster({ stdio: ["ignore", output, output, "ipc"] }); - - // handle exceptions in the master thread - there shouldn't be many of these - // the IPC (inter process communication) channel closed exception can't seem - // to be caught in a try catch, and is inconsequential, so it is ignored - process.on("uncaughtException", ({ message, stack }): void => { - if (message !== "Channel closed") { - this.mainLog(red(message)); - if (stack) { - this.mainLog(`uncaught exception\n${red(stack)}`); - } - } - }); - - // a helpful cluster event called on the master thread each time a child process exits - on("exit", ({ process: { pid } }, code, signal) => { - const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; - this.mainLog(cyan(prompt)); - // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one - this.spawn(); - }); - - this.repl = this.initializeRepl(); - this.spawn(); - } - - /** - * Generates a blue UTC string associated with the time - * of invocation. - */ - private timestamp = () => blue(`[${new Date().toUTCString()}]`); - - /** - * A formatted, identified and timestamped log in color - */ - public mainLog = (...optionalParams: any[]) => { - console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams); - } - - /** - * A formatted, identified and timestamped log in color for non- - */ - private execLog = (...optionalParams: any[]) => { - console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams); - } - - /** - * If the caller has indicated an interest - * in being notified of this feature, creates - * a GUID for this session that can, for example, - * be used as authentication for killing the server - * (checked externally). - */ - private initializeSessionKey = async (): Promise => { - if (this.notifiers?.key) { - this.key = Utils.GenerateGuid(); - const success = await this.notifiers.key(this.key); - const statement = success ? green("distributed session key to recipients") : red("distribution of session key failed"); - this.mainLog(statement); - } - } - - /** - * At any arbitrary layer of nesting within the configuration objects, any single value that - * is not specified by the configuration is given the default counterpart. If, within an object, - * one peer is given by configuration and two are not, the one is preserved while the two are given - * the default value. - * @returns the composition of all of the assigned objects, much like Object.assign(), but with more - * granularity in the overwriting of nested objects - */ - private preciseAssign = (target: any, ...sources: any[]): any => { - for (const source of sources) { - this.preciseAssignHelper(target, source); - } - return target; - } - - private preciseAssignHelper = (target: any, source: any) => { - Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { - let targetValue: any, sourceValue: any; - if (sourceValue = source[property]) { - if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { - this.preciseAssignHelper(targetValue, sourceValue); - } else { - target[property] = sourceValue; - } - } - }); - } - - /** - * Reads in configuration .json file only once, in the master thread - * and pass down any variables the pertinent to the child processes as environment variables. - */ - private loadAndValidateConfiguration = (): Configuration => { - let config: Configuration; - try { - console.log(this.timestamp(), cyan("validating configuration...")); - config = JSON.parse(readFileSync('./session.config.json', 'utf8')); - const options = { - throwError: true, - allowUnknownAttributes: false - }; - // ensure all necessary and no excess information is specified by the configuration file - validate(config, configurationSchema, options); - config = this.preciseAssign({}, defaultConfig, config); - } catch (error) { - if (error instanceof ValidationError) { - console.log(red("\nSession configuration failed.")); - console.log("The given session.config.json configuration file is invalid."); - console.log(`${error.instance}: ${error.stack}`); - process.exit(0); - } else if (error.code === "ENOENT" && error.path === "./session.config.json") { - console.log(cyan("Loading default session parameters...")); - console.log("Consider including a session.config.json configuration file in your project root for customization."); - config = this.preciseAssign({}, defaultConfig); - } else { - console.log(red("\nSession configuration failed.")); - console.log("The following unknown error occurred during configuration."); - console.log(error.stack); - process.exit(0); - } - } finally { - const { identifiers } = config!; - Object.keys(identifiers).forEach(key => { - const resolved = key as keyof Identifiers; - const { text, color } = identifiers[resolved]; - identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`); - }); - return config!; - } - } - - /** - * Builds the repl that allows the following commands to be typed into stdin of the master thread. - */ - private initializeRepl = (): Repl => { - const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` }); - const boolean = /true|false/; - const number = /\d+/; - const letters = /[a-zA-Z]+/; - repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); - repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean")); - repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true")); - repl.registerCommand("set", [/polling/, number, boolean], args => { - const newPollingIntervalSeconds = Math.floor(Number(args[2])); - if (newPollingIntervalSeconds < 0) { - this.mainLog(red("the polling interval must be a non-negative integer")); - } else { - if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { - this.config.polling.intervalSeconds = newPollingIntervalSeconds; - if (args[3] === "true") { - this.activeWorker?.send({ newPollingIntervalSeconds }); - } - } - } - }); - return repl; - } - - private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); - - /** - * Attempts to kill the active worker gracefully, unless otherwise specified. - */ - private killActiveWorker = (graceful = true, isSessionEnd = false): void => { - if (this.activeWorker && !this.activeWorker.isDead()) { - if (graceful) { - this.activeWorker.send({ manualExit: { isSessionEnd } }); - } else { - this.activeWorker.process.kill(); - } - } - } - - /** - * Allows the caller to set the port at which the target (be it the server, - * the websocket, some other custom port) is listening. If an immediate restart - * is specified, this monitor will kill the active child and re-launch the server - * at the port. Otherwise, the updated port won't be used until / unless the child - * dies on its own and triggers a restart. - */ - private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => { - if (value > 1023 && value < 65536) { - this.config.ports[port] = value; - if (immediateRestart) { - this.killActiveWorker(); - } - } else { - this.mainLog(red(`${port} is an invalid port number`)); - } - } - - /** - * Kills the current active worker and proceeds to spawn a new worker, - * feeding in configuration information as environment variables. - */ - private spawn = (): void => { - const { - polling: { - route, - failureTolerance, - intervalSeconds - }, - ports - } = this.config; - this.killActiveWorker(); - this.activeWorker = fork({ - pollingRoute: route, - pollingFailureTolerance: failureTolerance, - serverPort: ports.server, - socketPort: ports.socket, - pollingIntervalSeconds: intervalSeconds, - session_key: this.key, - DB: process.env.DB - }); - this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`)); - // an IPC message handler that executes actions on the master thread when prompted by the active worker - this.activeWorker.on("message", async ({ lifecycle, action }) => { - if (action) { - const { message, args } = action as Monitor.Action; - console.log(this.timestamp(), `${this.config.identifiers.worker.text} action requested (${cyan(message)})`); - switch (message) { - case "kill": - const { reason, graceful, errorCode } = args; - this.killSession(reason, graceful, errorCode); - break; - case "notify_crash": - if (this.notifiers?.crash) { - const { error } = args; - const success = await this.notifiers.crash(error); - const statement = success ? green("distributed crash notification to recipients") : red("distribution of crash notification failed"); - this.mainLog(statement); - } - break; - case "set_port": - const { port, value, immediateRestart } = args; - this.setPort(port, value, immediateRestart); - break; - } - const handlers = this.onMessage[message]; - if (handlers) { - handlers.forEach(handler => handler({ message, args })); - } - } - if (lifecycle) { - console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${lifecycle})`); - } - }); - } - - } - - /** - * Effectively, each worker repairs the connection to the server by reintroducing a consistent state - * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification - * email if the server encounters an uncaught exception or if the server cannot be reached. - */ - export class ServerWorker { - - private static count = 0; - private shouldServerBeResponsive = false; - private exitHandlers: ExitHandler[] = []; - private pollingFailureCount = 0; - private pollingIntervalSeconds: number; - private pollingFailureTolerance: number; - private pollTarget: string; - private serverPort: number; - - public static Create(work: Function) { - if (isMaster) { - console.error(red("cannot create a worker on the monitor process.")); - process.exit(1); - } else if (++ServerWorker.count > 1) { - process.send?.({ - action: { - message: "kill", args: { - reason: "cannot create more than one worker on a given worker process.", - graceful: false, - errorCode: 1 - } - } - }); - process.exit(1); - } else { - return new ServerWorker(work); - } - } - - /** - * Allows developers to invoke application specific logic - * by hooking into the exiting of the server process. - */ - public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); - - /** - * Kill the session monitor (parent process) from this - * server worker (child process). This will also kill - * this process (child process). - */ - public killSession = (reason: string, graceful = true, errorCode = 0) => this.sendMonitorAction("kill", { reason, graceful, errorCode }); - - /** - * A convenience wrapper to tell the session monitor (parent process) - * to carry out the action with the specified message and arguments. - */ - public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } }); - - private constructor(work: Function) { - this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); - - const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; - this.serverPort = Number(serverPort); - this.pollingIntervalSeconds = Number(pollingIntervalSeconds); - this.pollingFailureTolerance = Number(pollingFailureTolerance); - this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`; - - this.configureProcess(); - work(); - this.pollServer(); - } - - /** - * Set up message and uncaught exception handlers for this - * server process. - */ - private configureProcess = () => { - // updates the local values of variables to the those sent from master - process.on("message", async ({ newPollingIntervalSeconds, manualExit }) => { - if (newPollingIntervalSeconds !== undefined) { - this.pollingIntervalSeconds = newPollingIntervalSeconds; - } - if (manualExit !== undefined) { - const { isSessionEnd } = manualExit; - await this.executeExitHandlers(isSessionEnd); - process.exit(0); - } - }); - - // one reason to exit, as the process might be in an inconsistent state after such an exception - process.on('uncaughtException', this.proactiveUnplannedExit); - process.on('unhandledRejection', reason => { - const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); - this.proactiveUnplannedExit(appropriateError); - }); - } - - /** - * Execute the list of functions registered to be called - * whenever the process exits. - */ - private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); - - /** - * Notify master thread (which will log update in the console) of initialization via IPC. - */ - public lifecycleNotification = (event: string) => process.send?.({ lifecycle: event }); - - /** - * Called whenever the process has a reason to terminate, either through an uncaught exception - * in the process (potentially inconsistent state) or the server cannot be reached. - */ - private proactiveUnplannedExit = async (error: Error): Promise => { - this.shouldServerBeResponsive = false; - // communicates via IPC to the master thread that it should dispatch a crash notification email - this.sendMonitorAction("notify_crash", { error }); - await this.executeExitHandlers(error); - // notify master thread (which will log update in the console) of crash event via IPC - this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`)); - this.lifecycleNotification(red(error.message)); - process.exit(1); - } - - /** - * This monitors the health of the server by submitting a get request to whatever port / route specified - * by the configuration every n seconds, where n is also given by the configuration. - */ - private pollServer = async (): Promise => { - await new Promise(resolve => { - setTimeout(async () => { - try { - await get(this.pollTarget); - if (!this.shouldServerBeResponsive) { - // notify monitor thread that the server is up and running - this.lifecycleNotification(green(`listening on ${this.serverPort}...`)); - } - this.shouldServerBeResponsive = true; - } catch (error) { - // if we expect the server to be unavailable, i.e. during compilation, - // the listening variable is false, activeExit will return early and the child - // process will continue - if (this.shouldServerBeResponsive) { - if (++this.pollingFailureCount > this.pollingFailureTolerance) { - this.proactiveUnplannedExit(error); - } else { - this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`)); - } - } - } finally { - resolve(); - } - }, 1000 * this.pollingIntervalSeconds); - }); - // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed - this.pollServer(); - } - - } - -} -- cgit v1.2.3-70-g09d2 From de0c00ff0bb1f58a0736da24acc984f5a090d009 Mon Sep 17 00:00:00 2001 From: Stanley Yip 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/pen-gestures') 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 { + 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) => void, + ): MultiTouchEventDisposer { + const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent>).detail); + // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent>).detail) : undefined; + // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent>).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, ignorePen: boolean): React.Touch[] { const myTouches = new Array(); - 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
-
+
this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> 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>("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>("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>("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 = ; + runInAction(() => { + this._palette = ; + }); } - 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("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 ( -
+
{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 extends React.Component { //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 = new Map(); @@ -22,16 +25,27 @@ export abstract class Touchable extends React.Component { * 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): 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 extends React.Component { } }); + // 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 extends React.Component { * 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): 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 extends React.Component { } @action - protected onTouchEnd = (e: TouchEvent): void => { + protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent): 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 extends React.Component { console.log("clear"); } this._touchDrag = false; - e.stopPropagation(); + te.stopPropagation(); // if (e.targetTouches.length === 0) { @@ -126,11 +147,12 @@ export abstract class Touchable extends React.Component { 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 extends React.Component { } 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>).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>).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(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent(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
+ 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} 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(Docu private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _titleRef = React.createRef(); + 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(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(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(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(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(Docu width: animwidth, height: animheight, opacity: this.Document.opacity - }} onTouchStart={this.onTouchStart}> + }}> {this.innards}
; } 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 b04bb12a5942d97eef369936e30b36db62b51f30 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Mon, 27 Jan 2020 19:08:05 -0500 Subject: pen updates --- src/client/util/InteractionUtils.tsx | 5 ++- src/client/views/GestureOverlay.tsx | 52 +++++++++++++++++++--- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 28 ++++++------ src/pen-gestures/ndollar.ts | 7 ++- 5 files changed, 72 insertions(+), 22 deletions(-) (limited to 'src/pen-gestures') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 1fe95474c..ad1d1b5de 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -63,7 +63,7 @@ export namespace InteractionUtils { export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent, prevPoints: Map, ignorePen: boolean): React.Touch[] { const myTouches = new Array(); for (const pt of mte.touches) { - if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) { + if (!ignorePen || ((pt as any).radiusX > 1 && (pt as any).radiusY > 1)) { for (const tPt of mte.targetTouches) { if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) { if (pt && prevPoints.has(pt.identifier)) { @@ -73,6 +73,9 @@ export namespace InteractionUtils { } } } + if (mte.touches.length !== myTouches.length) { + throw Error("opo") + } return myTouches; } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index e8d685139..9dd87554f 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -20,6 +20,12 @@ import { DocumentView } from "./nodes/DocumentView"; import { Transform } from "../util/Transform"; import { DocumentContentsView } from "./nodes/DocumentContentsView"; +/** + * This class handles all of the gesture and touch events first. Native touch and pen + * events should be ignored by all classes and handled up here, and this class will interpret + * these events before dispatching our custom Dash gesture and touch events. Classes that want + * to use touch and pen events should handle custom Dash events, as opposed to native events. + */ @observer export default class GestureOverlay extends Touchable { static Instance: GestureOverlay; @@ -54,6 +60,15 @@ export default class GestureOverlay extends Touchable { GestureOverlay.Instance = this; } + /** + * @description + * Given a touch event, returns three arrays that represent the event's targetTouches, + * changedTouches, and touches after filtering out the touch events that are being handled + * as hands. This helps us separate hand events and touch events, as they are different + * events in the mental model that we are pursuing. + * @param e - Touch event to filter + * @returns \{ newTargetTouches, newChangedTouches, newTouches } + */ getNewTouches(e: React.TouchEvent | TouchEvent) { const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches); const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches); @@ -61,6 +76,7 @@ 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 there is a finger in this hand that matches the current point, ignore the current point if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) { ntt.splice(ntt.indexOf(pt), 1); } @@ -83,7 +99,11 @@ export default class GestureOverlay extends Touchable { return { ntt, nct, nt }; } + /** + * @description Handler for the native React touchStart event. + */ onReactTouchStart = (te: React.TouchEvent) => { + // clean up any ghost points that are remaining but don't actually exist const actualPts: React.Touch[] = []; for (let i = 0; i < te.touches.length; i++) { const pt: any = te.touches.item(i); @@ -91,9 +111,6 @@ export default class GestureOverlay extends Touchable { // 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); } } @@ -106,10 +123,11 @@ export default class GestureOverlay extends Touchable { }); ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); - const nts = this.getNewTouches(te); - console.log(nts.nt.length); + // decide whether we should be handling this as a hand event or a touch event + const nts = this.getNewTouches(te); if (nts.nt.length < 5) { + // dispatch a touch event const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); target?.dispatchEvent( new CustomEvent>("dashOnTouchStart", @@ -131,12 +149,17 @@ export default class GestureOverlay extends Touchable { document.addEventListener("touchend", this.onReactTouchEnd); } else { + // handle this event as a hand event this.handleHandDown(te); document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); } } + /** + * @description Handler for the native React touchMove event. Filters and dispatches + * the custom Dash touchMove event. + */ onReactTouchMove = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); document.dispatchEvent( @@ -154,7 +177,11 @@ export default class GestureOverlay extends Touchable { ); } + /** + * @description Handler for the native React touchEnd event. + */ onReactTouchEnd = (e: TouchEvent) => { + // filter and dispatch custom touchEnd event const nts: any = this.getNewTouches(e); document.dispatchEvent( new CustomEvent>("dashOnTouchEnd", @@ -169,6 +196,8 @@ export default class GestureOverlay extends Touchable { } }) ); + + // clean up any points that have ended for (let i = 0; i < e.changedTouches.length; i++) { const pt = e.changedTouches.item(i); if (pt) { @@ -178,6 +207,7 @@ export default class GestureOverlay extends Touchable { } } + // clean up events if (this.prevPoints.size === 0) { document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); @@ -185,8 +215,12 @@ export default class GestureOverlay extends Touchable { e.stopPropagation(); } + /** + * @description Handler for "handDown" events + */ handleHandDown = async (e: React.TouchEvent) => { const fingers = new Array(); + // log all the fingers on the hand for (let i = 0; i < e.touches.length; i++) { const pt: any = e.touches.item(i); if (pt.radiusX > 1 && pt.radiusY > 1) { @@ -200,6 +234,8 @@ export default class GestureOverlay extends Touchable { } } } + + // figure out left/right hand and thumb/pointer finger const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); const rightMost = Math.max(...fingers.map(f => f.clientX)); const leftMost = Math.min(...fingers.map(f => f.clientX)); @@ -229,6 +265,7 @@ export default class GestureOverlay extends Touchable { const minX = Math.min(...others.map(f => f.clientX)); const minY = Math.min(...others.map(f => f.clientY)); + // pull up the palette const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc); if (thumbDoc) { runInAction(() => { @@ -246,6 +283,9 @@ export default class GestureOverlay extends Touchable { document.addEventListener("touchend", this.handleHandUp); } + /** + * @description Handler for "handMove" event + */ @action handleHandMove = (e: TouchEvent) => { const fingers = new Array(); @@ -370,7 +410,7 @@ export default class GestureOverlay extends Touchable { else { const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; - if (result && result.Score > 0.7) { + if (result && result.Score > 0.8) { switch (result.Name) { case GestureUtils.Gestures.Box: const target = document.elementFromPoint(this._points[0].X, this._points[0].Y); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 132bf9c8e..a0c75c1b7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -347,7 +347,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); - e.stopPropagation(); + // e.stopPropagation(); } else { e.preventDefault(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e0913b154..82ee1bd63 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -316,21 +316,23 @@ export class DocumentView extends DocComponent(Docu handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (this.Document.onPointerDown) return; const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - this._downX = touch.clientX; - this._downY = touch.clientY; - if (!e.nativeEvent.cancelBubble) { - this._hitTemplateDrag = false; - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; + if (touch) { + this._downX = touch.clientX; + this._downY = touch.clientY; + if (!e.nativeEvent.cancelBubble) { + this._hitTemplateDrag = false; + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); } - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - e.stopPropagation(); } } diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index ef5ca38c6..9e15ada2d 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -168,7 +168,12 @@ export class NDollarRecognizer { // this.Multistrokes = new Array(NumMultistrokes); this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( - new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146)) + new Array( + new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200), + new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230), + new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160), + new Point(106, 146), //new Point(80, 150), new Point(50, 146), + new Point(30, 143)) )); this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array( new Array(new Point(12, 347), new Point(119, 347)) -- cgit v1.2.3-70-g09d2