From 2781f77d46612720865ac0c1d115d6e013806103 Mon Sep 17 00:00:00 2001 From: yipstanley Date: Tue, 3 Dec 2019 17:34:58 -0500 Subject: improved multi-user touch stuff --- src/client/util/InteractionUtils.ts | 11 + src/client/views/Touchable.tsx | 12 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 13 +- src/client/views/nodes/DocumentView.tsx | 23 +- src/ndollar.ts | 519 +++++++++++++++++++++ 5 files changed, 558 insertions(+), 20 deletions(-) create mode 100644 src/ndollar.ts (limited to 'src') diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts index b7738e862..c24c70bca 100644 --- a/src/client/util/InteractionUtils.ts +++ b/src/client/util/InteractionUtils.ts @@ -8,6 +8,17 @@ export namespace InteractionUtils { const REACT_POINTER_PEN_BUTTON = 0; const ERASER_BUTTON = 5; + export function GetMyTargetTouches(e: TouchEvent, prevPoints: Map): React.Touch[] { + let myTouches = new Array(); + for (let i = 0; i < e.targetTouches.length; i++) { + let pt = e.targetTouches.item(i); + if (pt && prevPoints.has(pt.identifier)) { + myTouches.push(pt); + } + } + return myTouches; + } + export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { switch (type) { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 0056a1d96..d0e3db8a5 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -32,6 +32,7 @@ export abstract class Touchable extends React.Component { break; case 2: this.handle2PointersDown(e); + break; } document.removeEventListener("touchmove", this.onTouch); @@ -46,10 +47,12 @@ export abstract class Touchable extends React.Component { */ @action protected onTouch = (e: TouchEvent): void => { + let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + // if we're not actually moving a lot, don't consider it as dragging yet // if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return; this._touchDrag = true; - switch (e.targetTouches.length) { + switch (myTouches.length) { case 1: this.handle1PointerMove(e); break; @@ -73,6 +76,7 @@ export abstract class Touchable extends React.Component { @action protected onTouchEnd = (e: TouchEvent): void => { + console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); this._touchDrag = false; e.stopPropagation(); @@ -81,6 +85,7 @@ export abstract class Touchable extends React.Component { let pt = e.targetTouches.item(i); if (pt) { if (this.prevPoints.has(pt.identifier)) { + console.log("delete"); this.prevPoints.delete(pt.identifier); } } @@ -89,7 +94,10 @@ export abstract class Touchable extends React.Component { if (e.targetTouches.length === 0) { this.prevPoints.clear(); } - this.cleanUpInteractions(); + + if (this.prevPoints.size === 0 && e.targetTouches.length === 0) { + this.cleanUpInteractions(); + } } cleanUpInteractions = (): void => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9506ce084..7267e94be 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -418,7 +418,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { handle1PointerMove = (e: TouchEvent) => { // panning a workspace if (!e.cancelBubble) { - let pt = e.targetTouches.item(0); + let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + let pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { @@ -443,9 +444,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { handle2PointersMove = (e: TouchEvent) => { // pinch zooming if (!e.cancelBubble) { - let pt1: Touch | null = e.targetTouches.item(0); - let pt2: Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; + let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + let pt1 = myTouches[0]; + let pt2 = myTouches[1]; if (this.prevPoints.size === 2) { let oldPoint1 = this.prevPoints.get(pt1.identifier); @@ -923,8 +924,8 @@ class CollectionFreeFormViewPannableContents extends React.Component - {this.props.children()} + return
+ {this.props.children}
; } } \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 39a68f51e..38c46b258 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); @@ -651,16 +651,15 @@ export class DocumentView extends DocComponent(Docu @action handle2PointersMove = (e: TouchEvent) => { - let pt1 = e.targetTouches.item(0); - let pt2 = e.targetTouches.item(1); - if (pt1 && pt2 && this.prevPoints.has(pt1.identifier) && this.prevPoints.has(pt2.identifier)) { - let oldPoint1 = this.prevPoints.get(pt1.identifier); - let oldPoint2 = this.prevPoints.get(pt2.identifier); - let pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); - if (pinching !== 0) { - let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX)) - this.props.Document.width = newWidth; - } + let myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + let pt1 = myTouches[0]; + let pt2 = myTouches[1]; + let oldPoint1 = this.prevPoints.get(pt1.identifier); + let oldPoint2 = this.prevPoints.get(pt2.identifier); + let pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); + if (pinching !== 0) { + let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX)) + this.props.Document.width = newWidth; } } diff --git a/src/ndollar.ts b/src/ndollar.ts new file mode 100644 index 000000000..2d3903cc3 --- /dev/null +++ b/src/ndollar.ts @@ -0,0 +1,519 @@ +/** + * The $N Multistroke Recognizer (JavaScript version) + * Converted to TypeScript -syip2 + * + * Lisa Anthony, Ph.D. + * UMBC + * Information Systems Department + * 1000 Hilltop Circle + * Baltimore, MD 21250 + * lanthony@umbc.edu + * + * Jacob O. Wobbrock, Ph.D. + * The Information School + * University of Washington + * Seattle, WA 98195-2840 + * wobbrock@uw.edu + * + * The academic publications for the $N recognizer, and what should be + * used to cite it, are: + * + * Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke + * recognizer for user interface prototypes. Proceedings of Graphics + * Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto, + * Ontario: Canadian Information Processing Society, pp. 245-252. + * https://dl.acm.org/citation.cfm?id=1839258 + * + * Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and + * accurate multistroke recognizer. Proceedings of Graphics Interface + * (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario: + * Canadian Information Processing Society, pp. 117-120. + * https://dl.acm.org/citation.cfm?id=2305296 + * + * The Protractor enhancement was separately published by Yang Li and programmed + * here by Jacob O. Wobbrock and Lisa Anthony: + * + * Li, Y. (2010). Protractor: A fast and accurate gesture + * recognizer. Proceedings of the ACM Conference on Human + * Factors in Computing Systems (CHI '10). Atlanta, Georgia + * (April 10-15, 2010). New York: ACM Press, pp. 2169-2172. + * https://dl.acm.org/citation.cfm?id=1753654 + * + * This software is distributed under the "New BSD License" agreement: + * + * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony. + * All rights reserved. Last updated July 14, 2018. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the names of UMBC nor the University of Washington, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. +**/ + +// +// Point class +// +export class Point { + constructor(public X: number, public Y: number) { } +} + +// +// Rectangle class +// +export class Rectangle { + constructor(public X: number, public Y: number, public Width: number, public Height: number) { } +} + +// +// Unistroke class: a unistroke template +// +export class Unistroke { + public Points: Point[]; + public StartUnitVector: Point; + public Vector: number[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) { + this.Points = Resample(points, NumPoints); + var radians = IndicativeAngle(this.Points); + this.Points = RotateBy(this.Points, -radians); + this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold); + if (useBoundedRotationInvariance) { + this.Points = RotateBy(this.Points, +radians); // restore + } + this.Points = TranslateTo(this.Points, Origin); + this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex); + this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor + } +} +// +// Multistroke class: a container for unistrokes +// +export class Multistroke { + public NumStrokes: number; + public Unistrokes: Unistroke[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor + { + this.NumStrokes = strokes.length; // number of individual strokes + + var order = new Array(strokes.length); // array of integer indices + for (var i = 0; i < strokes.length; i++) { + order[i] = i; // initialize + } + var orders = new Array(); // array of integer arrays + HeapPermute(strokes.length, order, /*out*/ orders); + + var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays + this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke + for (var j = 0; j < unistrokes.length; j++) { + this.Unistrokes[j] = new Unistroke(name, useBoundedRotationInvariance, unistrokes[j]); + } + } +} + +// +// Result class +// +export class Result { + constructor(public Name: string, public Score: any, public Time: any) { } +} + +// +// NDollarRecognizer constants +// +const NumMultistrokes = 16; +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("T", useBoundedRotationInvariance, new Array( + new Array(new Point(30, 7), new Point(103, 7)), + new Array(new Point(66, 7), new Point(66, 87)) + )); + this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( + new Array(new Point(177, 92), new Point(177, 2)), + new Array(new Point(182, 1), new Point(246, 95)), + new Array(new Point(247, 87), new Point(247, 1)) + )); + this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( + new Array(new Point(345, 9), new Point(345, 87)), + new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) + )); + this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( + new Array(new Point(507, 8), new Point(507, 87)), + new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) + )); + this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( + new Array(new Point(30, 146), new Point(106, 222)), + new Array(new Point(30, 225), new Point(106, 146)) + )); + this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( + new Array(new Point(188, 137), new Point(188, 225)), + new Array(new Point(188, 180), new Point(241, 180)), + new Array(new Point(241, 137), new Point(241, 225)) + )); + this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( + new Array(new Point(371, 149), new Point(371, 221)), + new Array(new Point(341, 149), new Point(401, 149)), + new Array(new Point(341, 221), new Point(401, 221)) + )); + this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( + new Array(new Point(526, 142), new Point(526, 204)), + new Array(new Point(526, 221)) + )); + this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( + new Array(new Point(12, 347), new Point(119, 347)) + )); + this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( + new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) + )); + this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( + new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), + new Array(new Point(418, 309), new Point(337, 390)) + )); + this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( + new Array(new Point(506, 349), new Point(574, 349)), + new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) + )); + this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( + new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), + new Array(new Point(62, 464), new Point(62, 571)) + )); + this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( + new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), + new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) + )); + this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( + new Array(new Point(325, 499), new Point(417, 557)), + new Array(new Point(417, 499), new Point(325, 557)), + new Array(new Point(371, 486), new Point(371, 571)) + )); + this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( + new Array(new Point(546, 465), new Point(546, 531)), + new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) + )); + // + // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures() + // + } + + Recognize = (strokes: any[], useBoundedRotationInvariance: boolean, requireSameNoOfStrokes: boolean, useProtractor: boolean) => { + 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) ? new Result("No match.", 0.0, t1 - t0) : 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 cb2edb837ffa9df5ad966d44ac0c26622d6579f0 Mon Sep 17 00:00:00 2001 From: yipstanley Date: Tue, 3 Dec 2019 17:57:37 -0500 Subject: first hint of recognizer? --- src/Utils.ts | 3 + .../collectionFreeForm/CollectionFreeFormView.tsx | 13 +- src/ndollar.ts | 148 +++++++++++---------- 3 files changed, 93 insertions(+), 71 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index 37b509370..dc62d96e0 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,9 +3,12 @@ import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message } from './server/Message'; import { RouteStore } from './server/RouteStore'; +import { NDollarRecognizer } from './ndollar'; export namespace Utils { + export const GestureRecognizer = new NDollarRecognizer(false); + export const DRAG_THRESHOLD = 4; export function GenerateGuid(): string { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7267e94be..dcda13b00 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -337,9 +337,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (this._points.length > 1) { let B = this.svgBounds; let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top })); - let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); - this.addDocument(inkDoc); - this._points = []; + + let result = Utils.GestureRecognizer.Recognize(new Array(points)); + if (result) { + console.log(result.Name); + } + else { + let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); + this.addDocument(inkDoc); + this._points = []; + } } document.removeEventListener("pointermove", this.onPointerMove); diff --git a/src/ndollar.ts b/src/ndollar.ts index 2d3903cc3..6d7b8c5d9 100644 --- a/src/ndollar.ts +++ b/src/ndollar.ts @@ -130,6 +130,10 @@ export class Multistroke { } } +export enum Gestures { + Box = "box" +} + // // Result class // @@ -140,7 +144,7 @@ export class Result { // // NDollarRecognizer constants // -const NumMultistrokes = 16; +const NumMultistrokes = 1; const NumPoints = 96; const SquareSize = 250.0; const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) @@ -165,78 +169,86 @@ export class NDollarRecognizer { // one predefined multistroke for each multistroke type // this.Multistrokes = new Array(NumMultistrokes); - this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array( - new Array(new Point(30, 7), new Point(103, 7)), - new Array(new Point(66, 7), new Point(66, 87)) - )); - this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( - new Array(new Point(177, 92), new Point(177, 2)), - new Array(new Point(182, 1), new Point(246, 95)), - new Array(new Point(247, 87), new Point(247, 1)) - )); - this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( - new Array(new Point(345, 9), new Point(345, 87)), - new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) - )); - this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( - new Array(new Point(507, 8), new Point(507, 87)), - new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) - )); - this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( - new Array(new Point(30, 146), new Point(106, 222)), - new Array(new Point(30, 225), new Point(106, 146)) - )); - this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( - new Array(new Point(188, 137), new Point(188, 225)), - new Array(new Point(188, 180), new Point(241, 180)), - new Array(new Point(241, 137), new Point(241, 225)) - )); - this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( - new Array(new Point(371, 149), new Point(371, 221)), - new Array(new Point(341, 149), new Point(401, 149)), - new Array(new Point(341, 221), new Point(401, 221)) - )); - this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( - new Array(new Point(526, 142), new Point(526, 204)), - new Array(new Point(526, 221)) - )); - this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( - new Array(new Point(12, 347), new Point(119, 347)) - )); - this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( - new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) - )); - this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( - new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), - new Array(new Point(418, 309), new Point(337, 390)) - )); - this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( - new Array(new Point(506, 349), new Point(574, 349)), - new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) - )); - this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( - new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), - new Array(new Point(62, 464), new Point(62, 571)) - )); - this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( - new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), - new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) - )); - this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( - new Array(new Point(325, 499), new Point(417, 557)), - new Array(new Point(417, 499), new Point(325, 557)), - new Array(new Point(371, 486), new Point(371, 571)) - )); - this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( - new Array(new Point(546, 465), new Point(546, 531)), - new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) + this.Multistrokes[0] = new Multistroke(Gestures.Box, useBoundedRotationInvariance, new Array( + new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146)) )); + + // + // PREDEFINED STROKES + // + + // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 7), new Point(103, 7)), + // new Array(new Point(66, 7), new Point(66, 87)) + // )); + // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 92), new Point(177, 2)), + // new Array(new Point(182, 1), new Point(246, 95)), + // new Array(new Point(247, 87), new Point(247, 1)) + // )); + // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( + // new Array(new Point(345, 9), new Point(345, 87)), + // new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) + // )); + // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( + // new Array(new Point(507, 8), new Point(507, 87)), + // new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) + // )); + // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 146), new Point(106, 222)), + // new Array(new Point(30, 225), new Point(106, 146)) + // )); + // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( + // new Array(new Point(188, 137), new Point(188, 225)), + // new Array(new Point(188, 180), new Point(241, 180)), + // new Array(new Point(241, 137), new Point(241, 225)) + // )); + // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( + // new Array(new Point(371, 149), new Point(371, 221)), + // new Array(new Point(341, 149), new Point(401, 149)), + // new Array(new Point(341, 221), new Point(401, 221)) + // )); + // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( + // new Array(new Point(526, 142), new Point(526, 204)), + // new Array(new Point(526, 221)) + // )); + // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( + // new Array(new Point(12, 347), new Point(119, 347)) + // )); + // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) + // )); + // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( + // new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), + // new Array(new Point(418, 309), new Point(337, 390)) + // )); + // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( + // new Array(new Point(506, 349), new Point(574, 349)), + // new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) + // )); + // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( + // new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), + // new Array(new Point(62, 464), new Point(62, 571)) + // )); + // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), + // new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) + // )); + // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( + // new Array(new Point(325, 499), new Point(417, 557)), + // new Array(new Point(417, 499), new Point(325, 557)), + // new Array(new Point(371, 486), new Point(371, 571)) + // )); + // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( + // new Array(new Point(546, 465), new Point(546, 531)), + // new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) + // )); // // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures() // } - Recognize = (strokes: any[], useBoundedRotationInvariance: boolean, requireSameNoOfStrokes: boolean, useProtractor: boolean) => { + Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => { var t0 = Date.now(); var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes var candidate = new Unistroke("", useBoundedRotationInvariance, points); @@ -267,7 +279,7 @@ export class NDollarRecognizer { } } var t1 = Date.now(); - return (u == -1) ? new Result("No match.", 0.0, t1 - t0) : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); + return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); } AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { -- cgit v1.2.3-70-g09d2 From d8b3a6fc2cc6cf15680de82cc512ef5e392bd375 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 3 Dec 2019 18:50:08 -0500 Subject: fixed text higlighing --- src/client/util/RichTextRules.ts | 8 ++++---- src/client/views/nodes/FormattedTextBox.tsx | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index bf365579a..cef1011cc 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -72,28 +72,28 @@ export const inpRules = { return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); }), new InputRule( - new RegExp(/t/), + new RegExp(/t$/), (state, match, start, end) => { if (state.selection.to === state.selection.from) return null; const node = (state.doc.resolve(start) as any).nodeAfter; return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), new InputRule( - new RegExp(/i/), + new RegExp(/i$/), (state, match, start, end) => { if (state.selection.to === state.selection.from) return null; const node = (state.doc.resolve(start) as any).nodeAfter; return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), new InputRule( - new RegExp(/\!/), + new RegExp(/!$/), (state, match, start, end) => { if (state.selection.to === state.selection.from) return null; const node = (state.doc.resolve(start) as any).nodeAfter; return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), new InputRule( - new RegExp(/\x/), + new RegExp(/x$/), (state, match, start, end) => { if (state.selection.to === state.selection.from) return null; const node = (state.doc.resolve(start) as any).nodeAfter; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f9b246c10..fcd8b6202 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -142,6 +142,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & constructor(props: any) { super(props); FormattedTextBox.Instance = this; + this.updateHighlights(); } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -191,6 +192,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks); const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); @@ -328,7 +330,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } return ret; } - static _highlights: string[] = []; + static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; updateHighlights = () => { clearStyleSheetRules(FormattedTextBox._userStyleSheet); @@ -370,6 +372,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & document.addEventListener("pointermove", this.sidebarMove); document.addEventListener("pointerup", this.sidebarUp); e.stopPropagation(); + e.preventDefault(); // prevents text from being selected during drag } sidebarMove = (e: PointerEvent) => { let bounds = this.CurrentDiv.getBoundingClientRect(); -- cgit v1.2.3-70-g09d2 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/Utils.ts | 4 - src/client/documents/Documents.ts | 2 +- src/client/views/InkingStroke.tsx | 8 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 22 +- src/ndollar.ts | 531 --------------------- src/new_fields/InkField.ts | 6 +- src/pen-gestures/GestureUtils.ts | 35 ++ src/pen-gestures/ndollar.ts | 529 ++++++++++++++++++++ 8 files changed, 584 insertions(+), 553 deletions(-) delete mode 100644 src/ndollar.ts create mode 100644 src/pen-gestures/GestureUtils.ts create mode 100644 src/pen-gestures/ndollar.ts (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index dc62d96e0..3db15f997 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,12 +3,8 @@ import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message } from './server/Message'; import { RouteStore } from './server/RouteStore'; -import { NDollarRecognizer } from './ndollar'; export namespace Utils { - - export const GestureRecognizer = new NDollarRecognizer(false); - export const DRAG_THRESHOLD = 4; export function GenerateGuid(): string { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c5bf109a1..8c6aa2006 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -422,7 +422,7 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options); } - export function InkDocument(color: string, tool: number, strokeWidth: number, points: { x: number, y: number }[], options: DocumentOptions = {}) { + export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) { let doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); doc.color = color; doc.strokeWidth = strokeWidth; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index a27f106e3..a2e9f0e55 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -13,8 +13,8 @@ import React = require("react"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); -export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) { - let pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, ""); +export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color?: string, width?: number) { + let pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); return ( p.x); - let ys = data.map(p => p.y); + let xs = data.map(p => p.X); + let ys = data.map(p => p.Y); let left = Math.min(...xs); let top = Math.min(...ys); let right = Math.max(...xs); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dcda13b00..6dd472c84 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -41,6 +41,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { computedFn, keepAlive } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; +import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -272,7 +273,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return clusterColor; } - @observable private _points: { x: number, y: number }[] = []; + @observable private _points: { X: number, Y: number }[] = []; @action onPointerDown = (e: React.PointerEvent): void => { @@ -288,7 +289,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { e.stopPropagation(); e.preventDefault(); let point = this.getTransform().transformPoint(e.pageX, e.pageY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } // if not using a pen and in no ink mode else if (InkingControl.Instance.selectedTool === InkTool.None) { @@ -336,11 +337,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (this._points.length > 1) { let B = this.svgBounds; - let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top })); + let points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); - let result = Utils.GestureRecognizer.Recognize(new Array(points)); + let result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); if (result) { console.log(result.Name); + this._points = []; } else { let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); @@ -405,7 +407,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const selectedTool = InkingControl.Instance.selectedTool; if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { let point = this.getTransform().transformPoint(e.clientX, e.clientY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } else if (selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { @@ -440,7 +442,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { let point = this.getTransform().transformPoint(pt.clientX, pt.clientY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } } e.stopPropagation(); @@ -846,8 +848,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @computed get svgBounds() { - let xs = this._points.map(p => p.x); - let ys = this._points.map(p => p.y); + let xs = this._points.map(p => p.X); + let ys = this._points.map(p => p.Y); let right = Math.max(...xs); let left = Math.min(...xs); let bottom = Math.max(...ys); @@ -863,7 +865,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let B = this.svgBounds; return ( - + {CreatePolyline(this._points, B.left, B.top)} ); @@ -932,7 +934,7 @@ class CollectionFreeFormViewPannableContents extends React.Component - {this.props.children} + {this.props.children()} ; } } \ No newline at end of file diff --git a/src/ndollar.ts b/src/ndollar.ts deleted file mode 100644 index 6d7b8c5d9..000000000 --- a/src/ndollar.ts +++ /dev/null @@ -1,531 +0,0 @@ -/** - * The $N Multistroke Recognizer (JavaScript version) - * Converted to TypeScript -syip2 - * - * Lisa Anthony, Ph.D. - * UMBC - * Information Systems Department - * 1000 Hilltop Circle - * Baltimore, MD 21250 - * lanthony@umbc.edu - * - * Jacob O. Wobbrock, Ph.D. - * The Information School - * University of Washington - * Seattle, WA 98195-2840 - * wobbrock@uw.edu - * - * The academic publications for the $N recognizer, and what should be - * used to cite it, are: - * - * Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke - * recognizer for user interface prototypes. Proceedings of Graphics - * Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto, - * Ontario: Canadian Information Processing Society, pp. 245-252. - * https://dl.acm.org/citation.cfm?id=1839258 - * - * Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and - * accurate multistroke recognizer. Proceedings of Graphics Interface - * (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario: - * Canadian Information Processing Society, pp. 117-120. - * https://dl.acm.org/citation.cfm?id=2305296 - * - * The Protractor enhancement was separately published by Yang Li and programmed - * here by Jacob O. Wobbrock and Lisa Anthony: - * - * Li, Y. (2010). Protractor: A fast and accurate gesture - * recognizer. Proceedings of the ACM Conference on Human - * Factors in Computing Systems (CHI '10). Atlanta, Georgia - * (April 10-15, 2010). New York: ACM Press, pp. 2169-2172. - * https://dl.acm.org/citation.cfm?id=1753654 - * - * This software is distributed under the "New BSD License" agreement: - * - * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony. - * All rights reserved. Last updated July 14, 2018. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the names of UMBC nor the University of Washington, - * nor the names of its contributors may be used to endorse or promote - * products derived from this software without specific prior written - * permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. -**/ - -// -// Point class -// -export class Point { - constructor(public X: number, public Y: number) { } -} - -// -// Rectangle class -// -export class Rectangle { - constructor(public X: number, public Y: number, public Width: number, public Height: number) { } -} - -// -// Unistroke class: a unistroke template -// -export class Unistroke { - public Points: Point[]; - public StartUnitVector: Point; - public Vector: number[]; - - constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) { - this.Points = Resample(points, NumPoints); - var radians = IndicativeAngle(this.Points); - this.Points = RotateBy(this.Points, -radians); - this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold); - if (useBoundedRotationInvariance) { - this.Points = RotateBy(this.Points, +radians); // restore - } - this.Points = TranslateTo(this.Points, Origin); - this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex); - this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor - } -} -// -// Multistroke class: a container for unistrokes -// -export class Multistroke { - public NumStrokes: number; - public Unistrokes: Unistroke[]; - - constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor - { - this.NumStrokes = strokes.length; // number of individual strokes - - var order = new Array(strokes.length); // array of integer indices - for (var i = 0; i < strokes.length; i++) { - order[i] = i; // initialize - } - var orders = new Array(); // array of integer arrays - HeapPermute(strokes.length, order, /*out*/ orders); - - var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays - this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke - for (var j = 0; j < unistrokes.length; j++) { - this.Unistrokes[j] = new Unistroke(name, useBoundedRotationInvariance, unistrokes[j]); - } - } -} - -export enum Gestures { - Box = "box" -} - -// -// Result class -// -export class Result { - constructor(public Name: string, public Score: any, public Time: any) { } -} - -// -// NDollarRecognizer constants -// -const NumMultistrokes = 1; -const NumPoints = 96; -const SquareSize = 250.0; -const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) -const Origin = new Point(0, 0); -const Diagonal = Math.sqrt(SquareSize * SquareSize + SquareSize * SquareSize); -const HalfDiagonal = 0.5 * Diagonal; -const AngleRange = Deg2Rad(45.0); -const AnglePrecision = Deg2Rad(2.0); -const Phi = 0.5 * (-1.0 + Math.sqrt(5.0)); // Golden Ratio -const StartAngleIndex = (NumPoints / 8); // eighth of gesture length -const AngleSimilarityThreshold = Deg2Rad(30.0); - -// -// NDollarRecognizer class -// -export class NDollarRecognizer { - public Multistrokes: Multistroke[]; - - constructor(useBoundedRotationInvariance: boolean) // constructor - { - // - // one predefined multistroke for each multistroke type - // - this.Multistrokes = new Array(NumMultistrokes); - this.Multistrokes[0] = new Multistroke(Gestures.Box, useBoundedRotationInvariance, new Array( - new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146)) - )); - - // - // PREDEFINED STROKES - // - - // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array( - // new Array(new Point(30, 7), new Point(103, 7)), - // new Array(new Point(66, 7), new Point(66, 87)) - // )); - // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( - // new Array(new Point(177, 92), new Point(177, 2)), - // new Array(new Point(182, 1), new Point(246, 95)), - // new Array(new Point(247, 87), new Point(247, 1)) - // )); - // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( - // new Array(new Point(345, 9), new Point(345, 87)), - // new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) - // )); - // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( - // new Array(new Point(507, 8), new Point(507, 87)), - // new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) - // )); - // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( - // new Array(new Point(30, 146), new Point(106, 222)), - // new Array(new Point(30, 225), new Point(106, 146)) - // )); - // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( - // new Array(new Point(188, 137), new Point(188, 225)), - // new Array(new Point(188, 180), new Point(241, 180)), - // new Array(new Point(241, 137), new Point(241, 225)) - // )); - // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( - // new Array(new Point(371, 149), new Point(371, 221)), - // new Array(new Point(341, 149), new Point(401, 149)), - // new Array(new Point(341, 221), new Point(401, 221)) - // )); - // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( - // new Array(new Point(526, 142), new Point(526, 204)), - // new Array(new Point(526, 221)) - // )); - // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( - // new Array(new Point(12, 347), new Point(119, 347)) - // )); - // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( - // new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) - // )); - // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( - // new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), - // new Array(new Point(418, 309), new Point(337, 390)) - // )); - // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( - // new Array(new Point(506, 349), new Point(574, 349)), - // new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) - // )); - // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( - // new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), - // new Array(new Point(62, 464), new Point(62, 571)) - // )); - // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( - // new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), - // new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) - // )); - // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( - // new Array(new Point(325, 499), new Point(417, 557)), - // new Array(new Point(417, 499), new Point(325, 557)), - // new Array(new Point(371, 486), new Point(371, 571)) - // )); - // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( - // new Array(new Point(546, 465), new Point(546, 531)), - // new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) - // )); - // - // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures() - // - } - - Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => { - var t0 = Date.now(); - var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes - var candidate = new Unistroke("", useBoundedRotationInvariance, points); - - var u = -1; - var b = +Infinity; - for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template - { - if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes - { - for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke - { - if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction - { - var d; - if (useProtractor) { - d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor - } - else { - d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N) - } - if (d < b) { - b = d; // best (least) distance - u = i; // multistroke owner of unistroke - } - } - } - } - } - var t1 = Date.now(); - return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); - } - - AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { - this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes); - var num = 0; - for (var i = 0; i < this.Multistrokes.length; i++) { - if (this.Multistrokes[i].Name == name) { - num++; - } - } - return num; - } - - DeleteUserGestures = () => { - this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set - return NumMultistrokes; - } -} - - -// -// Private helper functions from here on down -// -function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { - if (n == 1) { - orders[orders.length] = order.slice(); // append copy - } else { - for (var i = 0; i < n; i++) { - HeapPermute(n - 1, order, orders); - if (n % 2 == 1) { // swap 0, n-1 - var tmp = order[0]; - order[0] = order[n - 1]; - order[n - 1] = tmp; - } else { // swap i, n-1 - var tmp = order[i]; - order[i] = order[n - 1]; - order[n - 1] = tmp; - } - } - } -} - -function MakeUnistrokes(strokes: any, orders: any) { - var unistrokes = new Array(); // array of point arrays - for (var r = 0; r < orders.length; r++) { - for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions - { - var unistroke = new Array(); // array of points - for (var i = 0; i < orders[r].length; i++) { - var pts; - if (((b >> i) & 1) == 1) {// is b's bit at index i on? - pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse - } - else { - pts = strokes[orders[r][i]].slice(); // copy - } - for (var p = 0; p < pts.length; p++) { - unistroke[unistroke.length] = pts[p]; // append points - } - } - unistrokes[unistrokes.length] = unistroke; // add one unistroke to set - } - } - return unistrokes; -} - -function CombineStrokes(strokes: any) { - var points = new Array(); - for (var s = 0; s < strokes.length; s++) { - for (var p = 0; p < strokes[s].length; p++) - points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y); - } - return points; -} -function Resample(points: any, n: any) { - var I = PathLength(points) / (n - 1); // interval length - var D = 0.0; - var newpoints = new Array(points[0]); - for (var i = 1; i < points.length; i++) { - var d = Distance(points[i - 1], points[i]); - if ((D + d) >= I) { - var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X); - var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y); - var q = new Point(qx, qy); - newpoints[newpoints.length] = q; // append new point 'q' - points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i - D = 0.0; - } - else D += d; - } - if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so - newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y); - return newpoints; -} -function IndicativeAngle(points: any) { - var c = Centroid(points); - return Math.atan2(c.Y - points[0].Y, c.X - points[0].X); -} -function RotateBy(points: any, radians: any) // rotates points around centroid -{ - var c = Centroid(points); - var cos = Math.cos(radians); - var sin = Math.sin(radians); - var newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X - var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; - newpoints[newpoints.length] = new Point(qx, qy); - } - return newpoints; -} -function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D -{ - var B = BoundingBox(points); - var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test - var newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); - var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); - newpoints[newpoints.length] = new Point(qx, qy); - } - return newpoints; -} -function TranslateTo(points: any, pt: any) // translates points' centroid -{ - var c = Centroid(points); - var newpoints = new Array(); - for (var i = 0; i < points.length; i++) { - var qx = points[i].X + pt.X - c.X; - var qy = points[i].Y + pt.Y - c.Y; - newpoints[newpoints.length] = new Point(qx, qy); - } - return newpoints; -} -function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor -{ - var cos = 1.0; - var sin = 0.0; - if (useBoundedRotationInvariance) { - var iAngle = Math.atan2(points[0].Y, points[0].X); - var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0)); - cos = Math.cos(baseOrientation - iAngle); - sin = Math.sin(baseOrientation - iAngle); - } - var sum = 0.0; - var vector = new Array(); - for (var i = 0; i < points.length; i++) { - var newX = points[i].X * cos - points[i].Y * sin; - var newY = points[i].Y * cos + points[i].X * sin; - vector[vector.length] = newX; - vector[vector.length] = newY; - sum += newX * newX + newY * newY; - } - var magnitude = Math.sqrt(sum); - for (var i = 0; i < vector.length; i++) { - vector[i] /= magnitude; - } - return vector; -} -function OptimalCosineDistance(v1: any, v2: any) // for Protractor -{ - var a = 0.0; - var b = 0.0; - for (var i = 0; i < v1.length; i += 2) { - a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1]; - b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i]; - } - var angle = Math.atan(b / a); - return Math.acos(a * Math.cos(angle) + b * Math.sin(angle)); -} -function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) { - var x1 = Phi * a + (1.0 - Phi) * b; - var f1 = DistanceAtAngle(points, T, x1); - var x2 = (1.0 - Phi) * a + Phi * b; - var f2 = DistanceAtAngle(points, T, x2); - while (Math.abs(b - a) > threshold) { - if (f1 < f2) { - b = x2; - x2 = x1; - f2 = f1; - x1 = Phi * a + (1.0 - Phi) * b; - f1 = DistanceAtAngle(points, T, x1); - } else { - a = x1; - x1 = x2; - f1 = f2; - x2 = (1.0 - Phi) * a + Phi * b; - f2 = DistanceAtAngle(points, T, x2); - } - } - return Math.min(f1, f2); -} -function DistanceAtAngle(points: any, T: any, radians: any) { - var newpoints = RotateBy(points, radians); - return PathDistance(newpoints, T.Points); -} -function Centroid(points: any) { - var x = 0.0, y = 0.0; - for (var i = 0; i < points.length; i++) { - x += points[i].X; - y += points[i].Y; - } - x /= points.length; - y /= points.length; - return new Point(x, y); -} -function BoundingBox(points: any) { - var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity; - for (var i = 0; i < points.length; i++) { - minX = Math.min(minX, points[i].X); - minY = Math.min(minY, points[i].Y); - maxX = Math.max(maxX, points[i].X); - maxY = Math.max(maxY, points[i].Y); - } - return new Rectangle(minX, minY, maxX - minX, maxY - minY); -} -function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths -{ - var d = 0.0; - for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length - d += Distance(pts1[i], pts2[i]); - return d / pts1.length; -} -function PathLength(points: any) // length traversed by a point path -{ - var d = 0.0; - for (var i = 1; i < points.length; i++) - d += Distance(points[i - 1], points[i]); - return d; -} -function Distance(p1: any, p2: any) // distance between two points -{ - var dx = p2.X - p1.X; - var dy = p2.Y - p1.Y; - return Math.sqrt(dx * dx + dy * dy); -} -function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector -{ - var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y); - var len = Math.sqrt(v.X * v.X + v.Y * v.Y); - return new Point(v.X / len, v.Y / len); -} -function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2 -{ - var n = (v1.X * v2.X + v1.Y * v2.Y); - var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1] - return Math.acos(c); // arc cosine of the vector dot product -} -function Deg2Rad(d: any) { return (d * Math.PI / 180.0); } \ No newline at end of file diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 2d8bb582a..83d631958 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -13,14 +13,14 @@ export enum InkTool { } export interface PointData { - x: number; - y: number; + X: number; + Y: number; } export type InkData = Array; const pointSchema = createSimpleSchema({ - x: true, y: true + X: true, Y: true }); const strokeDataSchema = createSimpleSchema({ diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts new file mode 100644 index 000000000..caaa0736d --- /dev/null +++ b/src/pen-gestures/GestureUtils.ts @@ -0,0 +1,35 @@ +import { NDollarRecognizer } from "./ndollar"; +import { Type } from "typescript"; +import { InkField } from "../new_fields/InkField"; +import { Docs } from "../client/documents/Documents"; +import { Doc } from "../new_fields/Doc"; + +export namespace GestureUtils { + namespace GestureDataTypes { + export type BoxData = Doc[]; + } + + export const GestureRecognizer = new NDollarRecognizer(false); + + export enum Gestures { + Box = "box" + } + + export function GestureOptions(name: Gestures, gestureData: any): (() => any)[] { + switch (name) { + case Gestures.Box: + if (gestureData as GestureDataTypes.BoxData) { + return BoxOptions(gestureData as GestureDataTypes.BoxData); + } + break; + } + throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts"); + } + + function BoxOptions(gestureData: GestureDataTypes.BoxData): (() => any)[] { + if (gestureData instanceof Doc[]) { + return [() => Docs.Create.FreeformDocument(gestureData as Doc[], {})]; + } + return []; + } +} \ No newline at end of file diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts new file mode 100644 index 000000000..5d4384202 --- /dev/null +++ b/src/pen-gestures/ndollar.ts @@ -0,0 +1,529 @@ +import { GestureUtils } from "./GestureUtils"; + +/** + * The $N Multistroke Recognizer (JavaScript version) + * Converted to TypeScript -syip2 + * + * Lisa Anthony, Ph.D. + * UMBC + * Information Systems Department + * 1000 Hilltop Circle + * Baltimore, MD 21250 + * lanthony@umbc.edu + * + * Jacob O. Wobbrock, Ph.D. + * The Information School + * University of Washington + * Seattle, WA 98195-2840 + * wobbrock@uw.edu + * + * The academic publications for the $N recognizer, and what should be + * used to cite it, are: + * + * Anthony, L. and Wobbrock, J.O. (2010). A lightweight multistroke + * recognizer for user interface prototypes. Proceedings of Graphics + * Interface (GI '10). Ottawa, Ontario (May 31-June 2, 2010). Toronto, + * Ontario: Canadian Information Processing Society, pp. 245-252. + * https://dl.acm.org/citation.cfm?id=1839258 + * + * Anthony, L. and Wobbrock, J.O. (2012). $N-Protractor: A fast and + * accurate multistroke recognizer. Proceedings of Graphics Interface + * (GI '12). Toronto, Ontario (May 28-30, 2012). Toronto, Ontario: + * Canadian Information Processing Society, pp. 117-120. + * https://dl.acm.org/citation.cfm?id=2305296 + * + * The Protractor enhancement was separately published by Yang Li and programmed + * here by Jacob O. Wobbrock and Lisa Anthony: + * + * Li, Y. (2010). Protractor: A fast and accurate gesture + * recognizer. Proceedings of the ACM Conference on Human + * Factors in Computing Systems (CHI '10). Atlanta, Georgia + * (April 10-15, 2010). New York: ACM Press, pp. 2169-2172. + * https://dl.acm.org/citation.cfm?id=1753654 + * + * This software is distributed under the "New BSD License" agreement: + * + * Copyright (C) 2007-2011, Jacob O. Wobbrock and Lisa Anthony. + * All rights reserved. Last updated July 14, 2018. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the names of UMBC nor the University of Washington, + * nor the names of its contributors may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lisa Anthony OR Jacob O. Wobbrock + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. +**/ + +// +// Point class +// +export class Point { + constructor(public X: number, public Y: number) { } +} + +// +// Rectangle class +// +export class Rectangle { + constructor(public X: number, public Y: number, public Width: number, public Height: number) { } +} + +// +// Unistroke class: a unistroke template +// +export class Unistroke { + public Points: Point[]; + public StartUnitVector: Point; + public Vector: number[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) { + this.Points = Resample(points, NumPoints); + var radians = IndicativeAngle(this.Points); + this.Points = RotateBy(this.Points, -radians); + this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold); + if (useBoundedRotationInvariance) { + this.Points = RotateBy(this.Points, +radians); // restore + } + this.Points = TranslateTo(this.Points, Origin); + this.StartUnitVector = CalcStartUnitVector(this.Points, StartAngleIndex); + this.Vector = Vectorize(this.Points, useBoundedRotationInvariance); // for Protractor + } +} +// +// Multistroke class: a container for unistrokes +// +export class Multistroke { + public NumStrokes: number; + public Unistrokes: Unistroke[]; + + constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor + { + this.NumStrokes = strokes.length; // number of individual strokes + + var order = new Array(strokes.length); // array of integer indices + for (var i = 0; i < strokes.length; i++) { + order[i] = i; // initialize + } + var orders = new Array(); // array of integer arrays + HeapPermute(strokes.length, order, /*out*/ orders); + + var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays + this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke + for (var j = 0; j < unistrokes.length; j++) { + this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]); + } + } +} + +// +// Result class +// +export class Result { + constructor(public Name: string, public Score: any, public Time: any) { } +} + +// +// NDollarRecognizer constants +// +const NumMultistrokes = 1; +const NumPoints = 96; +const SquareSize = 250.0; +const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) +const Origin = new Point(0, 0); +const Diagonal = Math.sqrt(SquareSize * SquareSize + SquareSize * SquareSize); +const HalfDiagonal = 0.5 * Diagonal; +const AngleRange = Deg2Rad(45.0); +const AnglePrecision = Deg2Rad(2.0); +const Phi = 0.5 * (-1.0 + Math.sqrt(5.0)); // Golden Ratio +const StartAngleIndex = (NumPoints / 8); // eighth of gesture length +const AngleSimilarityThreshold = Deg2Rad(30.0); + +// +// NDollarRecognizer class +// +export class NDollarRecognizer { + public Multistrokes: Multistroke[]; + + constructor(useBoundedRotationInvariance: boolean) // constructor + { + // + // one predefined multistroke for each multistroke type + // + this.Multistrokes = new Array(NumMultistrokes); + this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( + new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146)) + )); + + // + // PREDEFINED STROKES + // + + // this.Multistrokes[0] = new Multistroke("T", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 7), new Point(103, 7)), + // new Array(new Point(66, 7), new Point(66, 87)) + // )); + // this.Multistrokes[1] = new Multistroke("N", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 92), new Point(177, 2)), + // new Array(new Point(182, 1), new Point(246, 95)), + // new Array(new Point(247, 87), new Point(247, 1)) + // )); + // this.Multistrokes[2] = new Multistroke("D", useBoundedRotationInvariance, new Array( + // new Array(new Point(345, 9), new Point(345, 87)), + // new Array(new Point(351, 8), new Point(363, 8), new Point(372, 9), new Point(380, 11), new Point(386, 14), new Point(391, 17), new Point(394, 22), new Point(397, 28), new Point(399, 34), new Point(400, 42), new Point(400, 50), new Point(400, 56), new Point(399, 61), new Point(397, 66), new Point(394, 70), new Point(391, 74), new Point(386, 78), new Point(382, 81), new Point(377, 83), new Point(372, 85), new Point(367, 87), new Point(360, 87), new Point(355, 88), new Point(349, 87)) + // )); + // this.Multistrokes[3] = new Multistroke("P", useBoundedRotationInvariance, new Array( + // new Array(new Point(507, 8), new Point(507, 87)), + // new Array(new Point(513, 7), new Point(528, 7), new Point(537, 8), new Point(544, 10), new Point(550, 12), new Point(555, 15), new Point(558, 18), new Point(560, 22), new Point(561, 27), new Point(562, 33), new Point(561, 37), new Point(559, 42), new Point(556, 45), new Point(550, 48), new Point(544, 51), new Point(538, 53), new Point(532, 54), new Point(525, 55), new Point(519, 55), new Point(513, 55), new Point(510, 55)) + // )); + // this.Multistrokes[4] = new Multistroke("X", useBoundedRotationInvariance, new Array( + // new Array(new Point(30, 146), new Point(106, 222)), + // new Array(new Point(30, 225), new Point(106, 146)) + // )); + // this.Multistrokes[5] = new Multistroke("H", useBoundedRotationInvariance, new Array( + // new Array(new Point(188, 137), new Point(188, 225)), + // new Array(new Point(188, 180), new Point(241, 180)), + // new Array(new Point(241, 137), new Point(241, 225)) + // )); + // this.Multistrokes[6] = new Multistroke("I", useBoundedRotationInvariance, new Array( + // new Array(new Point(371, 149), new Point(371, 221)), + // new Array(new Point(341, 149), new Point(401, 149)), + // new Array(new Point(341, 221), new Point(401, 221)) + // )); + // this.Multistrokes[7] = new Multistroke("exclamation", useBoundedRotationInvariance, new Array( + // new Array(new Point(526, 142), new Point(526, 204)), + // new Array(new Point(526, 221)) + // )); + // this.Multistrokes[8] = new Multistroke("line", useBoundedRotationInvariance, new Array( + // new Array(new Point(12, 347), new Point(119, 347)) + // )); + // this.Multistrokes[9] = new Multistroke("five-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 396), new Point(223, 299), new Point(262, 396), new Point(168, 332), new Point(278, 332), new Point(184, 397)) + // )); + // this.Multistrokes[10] = new Multistroke("null", useBoundedRotationInvariance, new Array( + // new Array(new Point(382, 310), new Point(377, 308), new Point(373, 307), new Point(366, 307), new Point(360, 310), new Point(356, 313), new Point(353, 316), new Point(349, 321), new Point(347, 326), new Point(344, 331), new Point(342, 337), new Point(341, 343), new Point(341, 350), new Point(341, 358), new Point(342, 362), new Point(344, 366), new Point(347, 370), new Point(351, 374), new Point(356, 379), new Point(361, 382), new Point(368, 385), new Point(374, 387), new Point(381, 387), new Point(390, 387), new Point(397, 385), new Point(404, 382), new Point(408, 378), new Point(412, 373), new Point(416, 367), new Point(418, 361), new Point(419, 353), new Point(418, 346), new Point(417, 341), new Point(416, 336), new Point(413, 331), new Point(410, 326), new Point(404, 320), new Point(400, 317), new Point(393, 313), new Point(392, 312)), + // new Array(new Point(418, 309), new Point(337, 390)) + // )); + // this.Multistrokes[11] = new Multistroke("arrowhead", useBoundedRotationInvariance, new Array( + // new Array(new Point(506, 349), new Point(574, 349)), + // new Array(new Point(525, 306), new Point(584, 349), new Point(525, 388)) + // )); + // this.Multistrokes[12] = new Multistroke("pitchfork", useBoundedRotationInvariance, new Array( + // new Array(new Point(38, 470), new Point(36, 476), new Point(36, 482), new Point(37, 489), new Point(39, 496), new Point(42, 500), new Point(46, 503), new Point(50, 507), new Point(56, 509), new Point(63, 509), new Point(70, 508), new Point(75, 506), new Point(79, 503), new Point(82, 499), new Point(85, 493), new Point(87, 487), new Point(88, 480), new Point(88, 474), new Point(87, 468)), + // new Array(new Point(62, 464), new Point(62, 571)) + // )); + // this.Multistrokes[13] = new Multistroke("six-point star", useBoundedRotationInvariance, new Array( + // new Array(new Point(177, 554), new Point(223, 476), new Point(268, 554), new Point(183, 554)), + // new Array(new Point(177, 490), new Point(223, 568), new Point(268, 490), new Point(183, 490)) + // )); + // this.Multistrokes[14] = new Multistroke("asterisk", useBoundedRotationInvariance, new Array( + // new Array(new Point(325, 499), new Point(417, 557)), + // new Array(new Point(417, 499), new Point(325, 557)), + // new Array(new Point(371, 486), new Point(371, 571)) + // )); + // this.Multistrokes[15] = new Multistroke("half-note", useBoundedRotationInvariance, new Array( + // new Array(new Point(546, 465), new Point(546, 531)), + // new Array(new Point(540, 530), new Point(536, 529), new Point(533, 528), new Point(529, 529), new Point(524, 530), new Point(520, 532), new Point(515, 535), new Point(511, 539), new Point(508, 545), new Point(506, 548), new Point(506, 554), new Point(509, 558), new Point(512, 561), new Point(517, 564), new Point(521, 564), new Point(527, 563), new Point(531, 560), new Point(535, 557), new Point(538, 553), new Point(542, 548), new Point(544, 544), new Point(546, 540), new Point(546, 536)) + // )); + // + // The $N Gesture Recognizer API begins here -- 3 methods: Recognize(), AddGesture(), and DeleteUserGestures() + // + } + + Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => { + var t0 = Date.now(); + var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes + var candidate = new Unistroke("", useBoundedRotationInvariance, points); + + var u = -1; + var b = +Infinity; + for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template + { + if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes + { + for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke + { + if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction + { + var d; + if (useProtractor) { + d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor + } + else { + d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N) + } + if (d < b) { + b = d; // best (least) distance + u = i; // multistroke owner of unistroke + } + } + } + } + } + var t1 = Date.now(); + return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0); + } + + AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => { + this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes); + var num = 0; + for (var i = 0; i < this.Multistrokes.length; i++) { + if (this.Multistrokes[i].Name == name) { + num++; + } + } + return num; + } + + DeleteUserGestures = () => { + this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set + return NumMultistrokes; + } +} + + +// +// Private helper functions from here on down +// +function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) { + if (n == 1) { + orders[orders.length] = order.slice(); // append copy + } else { + for (var i = 0; i < n; i++) { + HeapPermute(n - 1, order, orders); + if (n % 2 == 1) { // swap 0, n-1 + var tmp = order[0]; + order[0] = order[n - 1]; + order[n - 1] = tmp; + } else { // swap i, n-1 + var tmp = order[i]; + order[i] = order[n - 1]; + order[n - 1] = tmp; + } + } + } +} + +function MakeUnistrokes(strokes: any, orders: any) { + var unistrokes = new Array(); // array of point arrays + for (var r = 0; r < orders.length; r++) { + for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions + { + var unistroke = new Array(); // array of points + for (var i = 0; i < orders[r].length; i++) { + var pts; + if (((b >> i) & 1) == 1) {// is b's bit at index i on? + pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse + } + else { + pts = strokes[orders[r][i]].slice(); // copy + } + for (var p = 0; p < pts.length; p++) { + unistroke[unistroke.length] = pts[p]; // append points + } + } + unistrokes[unistrokes.length] = unistroke; // add one unistroke to set + } + } + return unistrokes; +} + +function CombineStrokes(strokes: any) { + var points = new Array(); + for (var s = 0; s < strokes.length; s++) { + for (var p = 0; p < strokes[s].length; p++) + points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y); + } + return points; +} +function Resample(points: any, n: any) { + var I = PathLength(points) / (n - 1); // interval length + var D = 0.0; + var newpoints = new Array(points[0]); + for (var i = 1; i < points.length; i++) { + var d = Distance(points[i - 1], points[i]); + if ((D + d) >= I) { + var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X); + var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y); + var q = new Point(qx, qy); + newpoints[newpoints.length] = q; // append new point 'q' + points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i + D = 0.0; + } + else D += d; + } + if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so + newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y); + return newpoints; +} +function IndicativeAngle(points: any) { + var c = Centroid(points); + return Math.atan2(c.Y - points[0].Y, c.X - points[0].X); +} +function RotateBy(points: any, radians: any) // rotates points around centroid +{ + var c = Centroid(points); + var cos = Math.cos(radians); + var sin = Math.sin(radians); + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X + var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y; + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D +{ + var B = BoundingBox(points); + var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width); + var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height); + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function TranslateTo(points: any, pt: any) // translates points' centroid +{ + var c = Centroid(points); + var newpoints = new Array(); + for (var i = 0; i < points.length; i++) { + var qx = points[i].X + pt.X - c.X; + var qy = points[i].Y + pt.Y - c.Y; + newpoints[newpoints.length] = new Point(qx, qy); + } + return newpoints; +} +function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor +{ + var cos = 1.0; + var sin = 0.0; + if (useBoundedRotationInvariance) { + var iAngle = Math.atan2(points[0].Y, points[0].X); + var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0)); + cos = Math.cos(baseOrientation - iAngle); + sin = Math.sin(baseOrientation - iAngle); + } + var sum = 0.0; + var vector = new Array(); + 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 018514e53f3d079780f5a9e559e75820af756975 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Dec 2019 20:14:21 -0500 Subject: fixed routing --- package.json | 2 +- src/server/Initialization.ts | 6 ++++-- src/server/RouteManager.ts | 5 ++--- src/server/authentication/config/passport.ts | 21 +-------------------- src/server/index.ts | 11 ++++++----- 5 files changed, 14 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index 499aefdb5..574c7e7fa 100644 --- a/package.json +++ b/package.json @@ -229,4 +229,4 @@ "xoauth2": "^1.2.0", "youtube": "^0.1.0" } -} \ No newline at end of file +} diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts index a41e2fea0..2e87c7a1a 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -39,8 +39,8 @@ export default async function InitializeServer(options: InitializationOptions) { app.use("/images", express.static(publicDirectory)); app.use("*", ({ user, originalUrl }, _res, next) => { - if (!originalUrl.includes("Heartbeat")) { - const userEmail = user && ("email" in user) ? user["email"] : undefined; + if (user && !originalUrl.includes("Heartbeat")) { + const userEmail = user.email; if (userEmail) { timeMap[userEmail] = Date.now(); } @@ -55,6 +55,7 @@ export default async function InitializeServer(options: InitializationOptions) { registerCorsProxy(app); const isRelease = determineEnvironment(); + routeSetter(new RouteManager(app, isRelease)); const server = app.listen(serverPort, () => { @@ -62,6 +63,7 @@ export default async function InitializeServer(options: InitializationOptions) { console.log(); }); addBeforeExitHandler(async () => { await new Promise(resolve => server.close(resolve)); }); + return isRelease; } diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index 41204964e..9e84b3687 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -67,9 +67,8 @@ export default class RouteManager { console.log('please remove all duplicate routes before continuing'); } if (malformedCount) { - console.log(`please ensure all routes adhere to ^\/[A-Za-z]+(\/\:[A-Za-z]+)*$`); + console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$`); } - console.log(); process.exit(0); } else { console.log(green("all server routes have been successfully registered:")); @@ -128,7 +127,7 @@ export default class RouteManager { } else { route = subscriber.build; } - if (!/^\/[A-Za-z]+(\/\:[A-Za-z]+)*$/g.test(route)) { + if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$/g.test(route)) { this.failedRegistrations.push({ reason: RegistrationError.Malformed, route diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 0ced99b0d..286209b20 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -1,8 +1,6 @@ import * as passport from 'passport'; import * as passportLocal from 'passport-local'; -import _ from "lodash"; import { default as User } from '../models/user_model'; -import { Request, Response, NextFunction } from "express"; const LocalStrategy = passportLocal.Strategy; @@ -28,21 +26,4 @@ passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true return done(undefined, user); }); }); -})); - -export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => { - if (req.isAuthenticated()) { - return next(); - } - return res.redirect("/login"); -}; - -export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { - const provider = req.path.split("/").slice(-1)[0]; - - if (_.find(req.user && "tokens" in req.user ? req.user["tokens"] : undefined, { kind: provider })) { - next(); - } else { - res.redirect(`/auth/${provider}`); - } -}; \ No newline at end of file +})); \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index cef6ff476..551ce3898 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -69,11 +69,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: // initialize API Managers console.log(yellow("\nregistering server routes...")); managers.forEach(manager => manager.register(addSupervisedRoute)); - logRegistrationOutcome(); - - // initialize the web socket (bidirectional communication: if a user changes - // a field on one client, that change must be broadcast to all other clients) - WebSocket.initialize(serverPort, isRelease); /** * Accessing root index redirects to home @@ -103,6 +98,12 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: } } }); + + logRegistrationOutcome(); + + // initialize the web socket (bidirectional communication: if a user changes + // a field on one client, that change must be broadcast to all other clients) + WebSocket.initialize(serverPort, isRelease); } (async function start() { -- cgit v1.2.3-70-g09d2 From 26fc5559314960c9cb186f56052b69e4f7d30f74 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 3 Dec 2019 21:43:47 -0500 Subject: secured root route access without authentication --- src/server/Initialization.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts index 2e87c7a1a..6fe67f2c7 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -38,13 +38,16 @@ export default async function InitializeServer(options: InitializationOptions) { app.use(express.static(publicDirectory)); app.use("/images", express.static(publicDirectory)); - app.use("*", ({ user, originalUrl }, _res, next) => { + app.use("*", ({ user, originalUrl }, res, next) => { if (user && !originalUrl.includes("Heartbeat")) { const userEmail = user.email; if (userEmail) { timeMap[userEmail] = Date.now(); } } + if (!user && originalUrl === "/") { + return res.redirect("/login"); + } next(); }); -- cgit v1.2.3-70-g09d2 From 97a4a9b1cfddc8b147ff61be13624704949c97f7 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 4 Dec 2019 12:50:38 -0500 Subject: tweaks to textbox sidebar. update to passport and npm --- package.json | 4 ++-- src/client/views/nodes/FormattedTextBox.scss | 1 + src/client/views/nodes/FormattedTextBox.tsx | 3 ++- src/server/Initialization.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/package.json b/package.json index 574c7e7fa..32344aad4 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@types/mongoose": "^5.5.8", "@types/node": "^10.12.30", "@types/nodemailer": "^4.6.6", - "@types/passport": "^1.0.0", + "@types/passport": "^1.0.2", "@types/passport-google-oauth20": "^2.0.2", "@types/passport-local": "^1.0.33", "@types/pdfjs-dist": "^2.0.0", @@ -171,7 +171,7 @@ "nodemailer": "^5.1.1", "nodemon": "^1.18.10", "normalize.css": "^8.0.1", - "npm": "^6.12.0", + "npm": "^6.13.2", "p-limit": "^2.2.0", "passport": "^0.4.0", "passport-google-oauth20": "^2.0.0", diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index c06f38a6c..f3a14169a 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -58,6 +58,7 @@ height: 35px; background: lightgray; border-radius: 20px; + cursor:grabbing; } .formattedTextBox-cont>.formattedTextBox-sidebar-handle { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fcd8b6202..481ae441e 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -197,6 +197,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); this._applyingChange = true; + this.extensionDoc && !this.extensionDoc.lastModified && (this.extensionDoc.backgroundColor = "lightGray"); this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); this._applyingChange = false; @@ -1131,7 +1132,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & e.stopPropagation(); }} > + style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> ); diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts index 6fe67f2c7..ff2b64317 100644 --- a/src/server/Initialization.ts +++ b/src/server/Initialization.ts @@ -40,7 +40,7 @@ export default async function InitializeServer(options: InitializationOptions) { app.use("*", ({ user, originalUrl }, res, next) => { if (user && !originalUrl.includes("Heartbeat")) { - const userEmail = user.email; + const userEmail = (user as any).email; if (userEmail) { timeMap[userEmail] = Date.now(); } -- cgit v1.2.3-70-g09d2 From 710dda13398c01b2b0f63b033ccca0c8ea4f7e49 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 4 Dec 2019 14:28:24 -0500 Subject: fixed mainview flyoutbehavior a bit. fixed keyvaluebox editing of lists a little. --- src/client/views/EditableView.tsx | 6 +++--- src/client/views/MainView.tsx | 2 +- src/client/views/collections/CollectionTreeView.tsx | 16 ++++++++++++++-- src/client/views/nodes/KeyValueBox.tsx | 8 ++++---- src/new_fields/DateField.ts | 4 ++++ src/new_fields/List.ts | 3 +-- 6 files changed, 27 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index f78b61892..ea9d548a1 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -10,7 +10,7 @@ export interface EditableProps { /** * Called to get the initial value for editing * */ - GetValue(): string; + GetValue(): string | undefined; /** * Called to apply changes @@ -108,8 +108,8 @@ export class EditableView extends React.Component { @action private finalizeEdit(value: string, shiftDown: boolean) { + this._editing = false; if (this.props.SetValue(value, shiftDown)) { - this._editing = false; this.props.isEditingCallback && this.props.isEditingCallback(false); } } @@ -124,7 +124,7 @@ export class EditableView extends React.Component { } render() { - if (this._editing) { + if (this._editing && this.props.GetValue() !== undefined) { return this.props.autosuggestProps ? { MainView.Instance._flyoutTranslate = true; - MainView.Instance.flyoutWidth = 250; + MainView.Instance.flyoutWidth = (MainView.Instance.flyoutWidth || 250); }); @computed get expandButton() { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 48ea35c6b..95503147a 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -279,7 +279,7 @@ class TreeView extends React.Component { const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; - if (contents instanceof Doc || Cast(contents, listSpec(Doc))) { + if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { const remDoc = (doc: Doc) => this.remove(doc, key); const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : @@ -294,7 +294,7 @@ class TreeView extends React.Component { height={13} fontSize={12} GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value)} />; + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; } rows.push(
{key + ":"} @@ -302,6 +302,18 @@ class TreeView extends React.Component { {contentElement}
); } + rows.push(
+ ""} + SetValue={(value: string) => { + value.indexOf(":") !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(":")), value.substring(value.indexOf(":") + 1, value.length), true); + return true; + }} /> +
); return rows; } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index fba29e4cd..322d639dc 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -66,10 +66,10 @@ export class KeyValueBox extends React.Component { return { script, type: dubEq, onDelegate: eq }; } - public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean { + public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = onDelegate ? doc : Doc.GetProto(doc); + const target = forceOnDelegate || onDelegate ? doc : Doc.GetProto(doc); let field: Field; if (type === "computed") { field = new ComputedField(script); @@ -88,10 +88,10 @@ export class KeyValueBox extends React.Component { } @undoBatch - public static SetField(doc: Doc, key: string, value: string) { + public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) { const script = this.CompileKVPScript(value); if (!script) return false; - return this.ApplyKVPScript(doc, key, script); + return this.ApplyKVPScript(doc, key, script, forceOnDelegate); } onPointerDown = (e: React.PointerEvent): void => { diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts index abec91e06..4f999e5e8 100644 --- a/src/new_fields/DateField.ts +++ b/src/new_fields/DateField.ts @@ -19,6 +19,10 @@ export class DateField extends ObjectField { return new DateField(this.date); } + toString() { + return `${this.date.toISOString()}`; + } + [ToScriptString]() { return `new DateField(new Date(${this.date.toISOString()}))`; } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index e9101158b..bb48b1bb3 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -290,8 +290,7 @@ class ListImpl extends ObjectField { private [SelfProxy]: any; [ToScriptString]() { - return "invalid"; - // return `new List([${(this as any).map((field => Field.toScriptString(field))}])`; + return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; } } export type List = ListImpl & (T | (T extends RefField ? Promise : never))[]; -- cgit v1.2.3-70-g09d2 From 74839056c8fccb216ee682c642771ab101b43d35 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 4 Dec 2019 14:34:17 -0500 Subject: from last --- src/client/views/nodes/FieldView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c93746773..048960c5e 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -53,7 +53,7 @@ export interface FieldViewProps { @observer export class FieldView extends React.Component { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`; //e.g., "" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "" } @computed -- cgit v1.2.3-70-g09d2 From 968d678e8aeb6f57cc892e3fe789067a8246c5fe Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 4 Dec 2019 17:47:28 -0500 Subject: started to add paths --- src/client/documents/Documents.ts | 5 +-- src/client/util/RichTextSchema.tsx | 3 +- src/client/views/CollectionLinearView.tsx | 1 + src/client/views/MainView.tsx | 11 ++++--- src/client/views/OverlayView.tsx | 1 + .../views/collections/CollectionDockingView.tsx | 38 +++++++++++++--------- .../views/collections/CollectionSchemaView.tsx | 1 + .../views/collections/CollectionStackingView.tsx | 1 + .../views/collections/CollectionTreeView.tsx | 36 +++++++++++--------- .../collectionFreeForm/CollectionFreeFormView.tsx | 1 + .../views/nodes/ContentFittingDocumentView.tsx | 4 ++- src/client/views/nodes/DocumentView.tsx | 12 ++++--- src/client/views/nodes/FieldView.tsx | 1 + src/client/views/nodes/FormattedTextBoxComment.tsx | 3 +- .../views/presentationview/PresElementBox.tsx | 3 +- src/client/views/search/SearchItem.tsx | 3 +- 16 files changed, 78 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e0f2858ba..5d5bdfcbd 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -534,7 +534,8 @@ export namespace Docs { export type DocConfig = { doc: Doc, - initialWidth?: number + initialWidth?: number, + path?: Doc[] }; export function StandardCollectionDockingDocument(configs: Array, options: DocumentOptions, id?: string, type: string = "row") { @@ -543,7 +544,7 @@ export namespace Docs { { type: type, content: [ - ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth)) + ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth, config.path)) ] } ] diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3b786e61d..4612f2885 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -761,8 +761,9 @@ export class DashDocView { this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px"; }); ReactDOM.render( this.openWorkspace(mainDoc), 0); @@ -271,6 +270,7 @@ export class MainView extends React.Component { {!mainContainer ? (null) : this.flyoutWidth; - addDocTabFunc = (doc: Doc, data: Opt, where: string) => { + addDocTabFunc = (doc: Doc, data: Opt, where: string, libraryPath?: Doc[]) => { if (where === "close") { return CollectionDockingView.CloseRightSplit(doc); } @@ -346,7 +346,7 @@ export class MainView extends React.Component { this.openWorkspace(doc); return true; } else { - return CollectionDockingView.AddRightSplit(doc, undefined); + return CollectionDockingView.AddRightSplit(doc, undefined, undefined, libraryPath); } } mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @@ -363,6 +363,7 @@ export class MainView extends React.Component { { @observable public static Instances: CollectionDockingView[] = []; @computed public static get Instance() { return CollectionDockingView.Instances[0]; } - public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { + public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number, libraryPath?: Doc[]) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -47,7 +47,8 @@ export class CollectionDockingView extends React.Component d[Id]) : [] //collectionDockingView: CollectionDockingView.Instance } }; @@ -95,12 +96,12 @@ export class CollectionDockingView extends React.Component { + public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => { Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument, undefined, libraryPath); if (stack === undefined) { let stack: any = this._goldenLayout.root; while (!stack.isStack) { @@ -427,10 +428,6 @@ export class CollectionDockingView extends React.Component, dragSpan); ReactDOM.render(, gearSpan); - // ReactDOM.render( { - // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); - // return true; - // }} />, upDiv); tab.reactComponents = [dragSpan, gearSpan, upDiv]; tab.element.append(dragSpan); tab.element.append(gearSpan); @@ -532,11 +529,13 @@ interface DockedFrameProps { documentId: FieldId; dataDocumentId: FieldId; glContainer: any; + libraryPath: (FieldId[]) //collectionDockingView: CollectionDockingView } @observer export class DockedFrameRenderer extends React.Component { _mainCont: HTMLDivElement | null = null; + @observable private _libraryPath: Doc[] = []; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt; @@ -554,6 +553,14 @@ export class DockedFrameRenderer extends React.Component { DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt) => this._dataDoc = f as Doc)); } })); + this.props.libraryPath && this.setupLibraryPath(); + } + + async setupLibraryPath() { + Promise.all(this.props.libraryPath.map(async docid => { + let d = await DocServer.GetRefField(docid); + return d instanceof Doc ? d : undefined + })).then(action((list: (Doc | undefined)[]) => this._libraryPath = list.filter(d => d).map(d => d as Doc))); } /** @@ -640,17 +647,17 @@ export class DockedFrameRenderer extends React.Component { get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } get widthpercent() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; } - addDocTab = (doc: Doc, dataDoc: Opt, location: string) => { + addDocTab = (doc: Doc, dataDoc: Opt, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); return true; } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, dataDoc); + return CollectionDockingView.AddRightSplit(doc, dataDoc, undefined, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); } else { - return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); + return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc, libraryPath); } } @@ -659,6 +666,7 @@ export class DockedFrameRenderer extends React.Component { const document = this._document; const resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; return doc) { doc) { return boolean; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; @@ -95,8 +96,8 @@ class TreeView extends React.Component { @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); } @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } @computed get fieldKey() { - const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\""); - return splits.length > 1 ? splits[1].split("\"")[0] : "data"; + const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); + return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; @@ -120,7 +121,7 @@ class TreeView extends React.Component { } @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"); + @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath); @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete(); @undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => { return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); @@ -194,8 +195,8 @@ class TreeView extends React.Component { ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.GetProto(CurrentUserUtils.UserDocument.recentlyClosed as Doc).data = new List(), icon: "plus" }); } else if (this.props.document !== CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab", this.props.libraryPath), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath), icon: "caret-square-right" }); if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.dataDoc)), icon: "camera" }); } @@ -286,7 +287,7 @@ class TreeView extends React.Component { DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, doc[Id]]); + [...this.props.renderedIds, doc[Id]], this.props.libraryPath); } else { contentElement = { this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, this.props.document[Id]])} + [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath)} ; } else if (this.treeViewExpandedView === "fields") { return
    @@ -343,6 +344,7 @@ class TreeView extends React.Component { { renderDepth: number, hideHeaderFields: () => boolean, preventTreeViewOpen: boolean, - renderedIds: string[] + renderedIds: string[], + libraryPath: Doc[] | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -499,9 +502,9 @@ class TreeView extends React.Component { } const indent = i === 0 ? undefined : () => { - if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) { - const fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1]; - const fieldKey = fieldKeysub.split("\"")[1]; + if (StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(docs[i - 1].layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) { Doc.AddDocToList(docs[i - 1], fieldKey, child); docs[i - 1].treeViewOpen = true; @@ -510,9 +513,9 @@ class TreeView extends React.Component { } }; const outdent = !parentCollectionDoc ? undefined : () => { - if (StrCast(parentCollectionDoc.layout).indexOf("fieldKey") !== -1) { - const fieldKeysub = StrCast(parentCollectionDoc.layout).split("fieldKey")[1]; - const fieldKey = fieldKeysub.split("\"")[1]; + if (StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); parentCollectionDoc.treeViewOpen = true; remove(child); @@ -529,6 +532,7 @@ class TreeView extends React.Component { return !(child instanceof Doc) ? (null) : BoolCast(this.props.Document.hideHeaderFields), - BoolCast(this.props.Document.preventTreeViewOpen), []) + BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath) }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index c69b264c4..c3e064da5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -627,6 +627,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ...this.props, DataDoc: childData, Document: childLayout, + LibraryPath: this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : [], layoutKey: undefined, ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index efc907f9b..86fab0ba0 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -17,6 +17,7 @@ import { CollectionView } from "../collections/CollectionView"; interface ContentFittingDocumentViewProps { Document?: Doc; DataDocument?: Doc; + LibraryPath: Doc[]; childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; @@ -85,8 +86,9 @@ export class ContentFittingDocumentView extends React.Component ; Document: Doc; DataDoc?: Doc; + LibraryPath: Doc[]; fitToBox?: boolean; onClick?: ScriptField; addDocument?: (doc: Doc) => boolean; @@ -70,7 +71,7 @@ export interface DocumentViewProps { parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; @@ -401,9 +402,9 @@ export class DocumentView extends DocComponent(Docu const cm = ContextMenu.Instance; const subitems: ContextMenuProps[] = []; - subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" }); - subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" }); + subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab", this.props.LibraryPath), icon: "folder" }); + subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" }); subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); @@ -527,6 +528,8 @@ export class DocumentView extends DocComponent(Docu SelectionManager.SelectDoc(this, false); } }); + let path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc().LibraryBtn as Doc).sourcePanel as Doc) ? "" : d.title), ""); + cm.addItem({ description: `path: ${path}`, event: () => { }, icon: "check" }) } // does Document set a layout prop @@ -552,6 +555,7 @@ export class DocumentView extends DocComponent(Docu ContainingCollectionDoc={this.props.ContainingCollectionDoc} Document={this.props.Document} fitToBox={this.props.fitToBox} + LibraryPath={this.props.LibraryPath} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 048960c5e..ce1c468ad 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -30,6 +30,7 @@ export interface FieldViewProps { ruleProvider: Doc | undefined; Document: Doc; DataDoc?: Doc; + LibraryPath: Doc[]; onClick?: ScriptField; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 2ec30e3b3..9f1dd4aec 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -177,8 +177,9 @@ export class FormattedTextBoxComment { } catch (e) { } if (target) { ReactDOM.render((P width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), }}> { onPointerEnter={action(() => this._displayDim = this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE))} onPointerLeave={action(() => this._displayDim = 50)} > Date: Thu, 5 Dec 2019 11:11:07 -0500 Subject: compile warning fixes. --- src/client/util/RichTextSchema.tsx | 6 +++--- .../views/collections/CollectionDockingView.tsx | 6 +++--- .../views/collections/CollectionSchemaCells.tsx | 1 + src/client/views/nodes/DocumentView.tsx | 9 +++++++-- src/client/views/nodes/FormattedTextBox.tsx | 4 ++-- src/client/views/nodes/ImageBox.scss | 23 +++++++++++----------- src/client/views/nodes/ImageBox.tsx | 7 ++++--- src/client/views/nodes/KeyValuePair.tsx | 1 + src/client/views/pdf/PDFViewer.tsx | 1 + src/new_fields/documentSchemas.ts | 3 +++ src/new_fields/util.ts | 2 +- 11 files changed, 37 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 4612f2885..189bf08f7 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -671,15 +671,15 @@ export class DashDocCommentView { this._collapsed.className = "formattedTextBox-inlineComment"; this._collapsed.id = "DashDocCommentView-" + node.attrs.docid; this._view = view; - let targetNode = () => { + const targetNode = () => { for (let i = getPos() + 1; i < view.state.doc.nodeSize; i++) { - let m = view.state.doc.nodeAt(i); + const m = view.state.doc.nodeAt(i); if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) { return { node: m, pos: i } as { node: any, pos: number }; } } return undefined; - } + }; this._collapsed.onpointerdown = (e: any) => { const target = targetNode(); if (target) { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e35dc4c00..4374cde3c 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -529,7 +529,7 @@ interface DockedFrameProps { documentId: FieldId; dataDocumentId: FieldId; glContainer: any; - libraryPath: (FieldId[]) + libraryPath: (FieldId[]); //collectionDockingView: CollectionDockingView } @observer @@ -558,8 +558,8 @@ export class DockedFrameRenderer extends React.Component { async setupLibraryPath() { Promise.all(this.props.libraryPath.map(async docid => { - let d = await DocServer.GetRefField(docid); - return d instanceof Doc ? d : undefined + const d = await DocServer.GetRefField(docid); + return d instanceof Doc ? d : undefined; })).then(action((list: (Doc | undefined)[]) => this._libraryPath = list.filter(d => d).map(d => d as Doc))); } diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 1700c14cf..171dc4606 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -143,6 +143,7 @@ export class CollectionSchemaCell extends React.Component { const props: FieldViewProps = { Document: this.props.rowProps.original, DataDoc: this.props.rowProps.original, + LibraryPath: [], fieldKey: this.props.rowProps.column.id as string, ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fef365bf3..9f5b86e8d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -528,8 +528,13 @@ export class DocumentView extends DocComponent(Docu SelectionManager.SelectDoc(this, false); } }); - let path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc().LibraryBtn as Doc).sourcePanel as Doc) ? "" : d.title), ""); - cm.addItem({ description: `path: ${path}`, event: () => { }, icon: "check" }) + const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc().LibraryBtn as Doc).sourcePanel as Doc) ? "" : d.title), ""); + cm.addItem({ + description: `path: ${path}`, event: () => { + this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true); + Doc.BrushDoc(this.props.Document); + }, icon: "check" + }); } // does Document set a layout prop diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 481ae441e..e7c59ccb4 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -376,8 +376,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & e.preventDefault(); // prevents text from being selected during drag } sidebarMove = (e: PointerEvent) => { - let bounds = this.CurrentDiv.getBoundingClientRect(); - this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY)) + const bounds = this.CurrentDiv.getBoundingClientRect(); + this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY)); this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; } sidebarUp = (e: PointerEvent) => { diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3b42c2352..cf5d999a7 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,13 +1,22 @@ -.imageBox { +.imageBox, .imageBox-dragging{ pointer-events: all; border-radius: inherit; width:100%; height:100%; position: absolute; transform-origin: top left; + .imageBox-fader { + pointer-events: all; + } } -.imageBox-cont, .imageBox-cont-dragging { +.imageBox-dragging { + .imageBox-fader { + pointer-events: none; + } +} + +.imageBox-cont { padding: 0vw; position: absolute; text-align: center; @@ -22,15 +31,6 @@ width: 100%; pointer-events: all; } - .imageBox-fader { - pointer-events: all; - } -} - -.imageBox-cont-dragging { - .imageBox-fader { - pointer-events: none; - } } .imageBox-dot { @@ -43,7 +43,6 @@ background: gray; } - #google-photos { transition: all 0.5s ease 0s; width: 30px; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b4a51657f..f60888929 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -308,7 +308,6 @@ export class ImageBox extends DocAnnotatableComponent + return
[this.content]; render() { - return (
{ const props: FieldViewProps = { Document: this.props.doc, DataDoc: this.props.doc, + LibraryPath: [], ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, ruleProvider: undefined, diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 69aacc902..3aa5d1d2c 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -634,6 +634,7 @@ export class PDFViewer extends DocAnnotatableComponent