From 37be2477782b0b372c16d46df3f9634ebf3677fe Mon Sep 17 00:00:00 2001
From: Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com>
Date: Wed, 21 Sep 2022 01:43:03 -0400
Subject: restyled document decorations

and changed novice mode to exclude `data` in treeView
---
 src/client/views/nodes/button/FontIconBox.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/nodes/button/FontIconBox.tsx')

diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 4a6099fb3..6d1751b25 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -275,7 +275,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     icon = 'globe-asia';
                     text = 'User Default';
                 }
-                noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
+                noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
             } else if (script?.script.originalScript.startsWith('setFont')) {
                 const editorView = RichTextMenu.Instance?.TextView?.EditorView;
                 text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
-- 
cgit v1.2.3-70-g09d2


From 0e3892e63758accd7dae274072ad7893934c3624 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Oct 2022 15:07:31 -0400
Subject: cleaned up gestureutils a bit.  added recognize gestures option, but
 it probably shouldn't be used until the geometry gesture recognition
 descriptions are improved.

---
 src/client/util/CurrentUserUtils.ts                |   8 +-
 src/client/util/InteractionUtils.tsx               |  26 ++-
 src/client/util/SettingsManager.tsx                |   4 +
 src/client/views/GestureOverlay.tsx                |  49 ++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   9 +-
 src/pen-gestures/GestureUtils.ts                   |  48 ++---
 src/pen-gestures/ndollar.ts                        | 230 ++++++++++++---------
 8 files changed, 204 insertions(+), 176 deletions(-)

(limited to 'src/client/views/nodes/button/FontIconBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 1c9f89fa0..bfa868dbc 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -12,6 +12,7 @@ import { ComputedField, ScriptField } from "../../fields/ScriptField";
 import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
 import { SetCachedGroups, SharingPermissions } from "../../fields/util";
+import { GestureUtils } from "../../pen-gestures/GestureUtils";
 import { OmitKeys, Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
 import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
@@ -631,9 +632,9 @@ export class CurrentUserUtils {
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} },
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
             // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} },
-            // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' },
-            { title: "Line",   toolTip: "Line (Ctrl+Shift+L)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:'{ return setActiveTool("line", _readOnly_);}' }},
+            { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (Ctrl+Shift+L)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
             { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
             { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
@@ -797,6 +798,7 @@ export class CurrentUserUtils {
         doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true);
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
+        doc.activeTool = InkTool.None;
         doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
         doc.activeInkWidth ?? (doc.activeInkWidth = 1);
         doc.activeInkBezier ?? (doc.activeInkBezier = "0");
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 85700da37..6622b498d 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,4 +1,5 @@
 import React = require('react');
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
 import { Utils } from '../../Utils';
 import './InteractionUtils.scss';
 
@@ -186,7 +187,7 @@ export namespace InteractionUtils {
     export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
         if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
             //pointer is up (first and last points are the same)
-            if (shape === 'arrow' || shape === 'line' || shape === 'circle') {
+            if (shape === GestureUtils.Gestures.Arrow || shape === GestureUtils.Gestures.Line || shape === GestureUtils.Gestures.Circle) {
                 //if arrow or line, the two end points should be the starting and the ending point
                 var left = points[0].X;
                 var top = points[0].Y;
@@ -208,7 +209,7 @@ export namespace InteractionUtils {
             left = points[0].X;
             bottom = points[points.length - 1].Y;
             top = points[0].Y;
-            if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') {
+            if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) {
                 //switch left/right and top/bottom if needed
                 if (left > right) {
                     const temp = right;
@@ -224,20 +225,20 @@ export namespace InteractionUtils {
         }
         points = [];
         switch (shape) {
-            case 'rectangle':
+            case GestureUtils.Gestures.Rectangle:
                 points.push({ X: left, Y: top });
                 points.push({ X: right, Y: top });
                 points.push({ X: right, Y: bottom });
                 points.push({ X: left, Y: bottom });
                 points.push({ X: left, Y: top });
                 break;
-            case 'triangle':
+            case GestureUtils.Gestures.Triangle:
                 points.push({ X: left, Y: bottom });
                 points.push({ X: right, Y: bottom });
                 points.push({ X: (right + left) / 2, Y: top });
                 points.push({ X: left, Y: bottom });
                 break;
-            case 'circle':
+            case GestureUtils.Gestures.Circle:
                 const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
                 const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
                 const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
@@ -253,7 +254,7 @@ export namespace InteractionUtils {
                 points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
                 break;
 
-            case 'line':
+            case GestureUtils.Gestures.Line:
                 points.push({ X: left, Y: top });
                 points.push({ X: right, Y: bottom });
                 break;
@@ -266,17 +267,14 @@ export namespace InteractionUtils {
      * @param type - InteractionUtils.(PENTYPE | ERASERTYPE | MOUSETYPE | TOUCHTYPE)
      */
     export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
+        // prettier-ignore
         switch (type) {
             // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
-            case PENTYPE:
-                return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
-            case ERASERTYPE:
-                return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
-            case TOUCHTYPE:
-                return e.pointerType === TOUCHTYPE;
-            default:
-                return e.pointerType === type;
+            case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+            case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+            case TOUCHTYPE: return e.pointerType === TOUCHTYPE;
         }
+        return e.pointerType === type;
     }
 
     /**
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index a185c8936..92032a8f9 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -186,6 +186,10 @@ export class SettingsManager extends React.Component<{}> {
                     <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} />
                     <div className="preferences-check">Show button labels</div>
                 </div>
+                <div>
+                    <input type="checkbox" onChange={e => FontIconBox.SetRecognizeGesturs(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} />
+                    <div className="preferences-check">Recognize ink Gesturs</div>
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 850688e7e..23b03bc50 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -684,7 +684,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             }
             //if any of the shape is activated in the CollectionFreeFormViewChrome
             else if (this.InkShape) {
-                this.makePolygon(this.InkShape, false);
+                this.makeBezierPolygon(this.InkShape, false);
                 this.dispatchGesture(GestureUtils.Gestures.Stroke);
                 this._points.length = 0;
                 if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
@@ -699,27 +699,17 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 let actionPerformed = false;
                 if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
-                        case GestureUtils.Gestures.Box:
-                            actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box);
-                            break;
+                        case GestureUtils.Gestures.Triangle:
+                        case GestureUtils.Gestures.Rectangle:
+                        case GestureUtils.Gestures.Circle:
+                            this.makeBezierPolygon(result.Name, true);
                         case GestureUtils.Gestures.StartBracket:
-                            actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket);
-                            break;
                         case GestureUtils.Gestures.EndBracket:
-                            actionPerformed = this.dispatchGesture('endbracket');
+                            actionPerformed = this.dispatchGesture(result.Name);
                             break;
                         case GestureUtils.Gestures.Line:
                             actionPerformed = this.handleLineGesture();
                             break;
-                        case GestureUtils.Gestures.Triangle:
-                            actionPerformed = this.makePolygon('triangle', true);
-                            break;
-                        case GestureUtils.Gestures.Circle:
-                            actionPerformed = this.makePolygon('circle', true);
-                            break;
-                        case GestureUtils.Gestures.Rectangle:
-                            actionPerformed = this.makePolygon('rectangle', true);
-                            break;
                         case GestureUtils.Gestures.Scribble:
                             console.log('scribble');
                             break;
@@ -760,11 +750,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         CollectionFreeFormViewChrome.Instance?.primCreated();
     };
 
-    makePolygon = (shape: string, gesture: boolean) => {
-        //take off gesture recognition for now
-        if (gesture) {
-            return false;
-        }
+    makeBezierPolygon = (shape: string, gesture: boolean) => {
         const xs = this._points.map(p => p.X);
         const ys = this._points.map(p => p.Y);
         var right = Math.max(...xs);
@@ -794,7 +780,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             left = this._points[0].X;
             bottom = this._points[this._points.length - 2].Y;
             top = this._points[0].Y;
-            if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') {
+            if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) {
                 if (left > right) {
                     const temp = right;
                     right = left;
@@ -809,9 +795,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         }
         this._points.length = 0;
         switch (shape) {
-            //must push an extra point in the end so InteractionUtils knows pointer is up.
-            //must be (points[0].X,points[0]-1)
-            case 'rectangle':
+            case GestureUtils.Gestures.Rectangle:
                 this._points.push({ X: left, Y: top });
                 this._points.push({ X: left, Y: top });
                 this._points.push({ X: right, Y: top });
@@ -834,7 +818,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
 
                 break;
 
-            case 'triangle':
+            case GestureUtils.Gestures.Triangle:
                 this._points.push({ X: left, Y: bottom });
                 this._points.push({ X: left, Y: bottom });
 
@@ -852,7 +836,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 this._points.push({ X: left, Y: bottom });
 
                 break;
-            case 'circle':
+            case GestureUtils.Gestures.Circle:
                 // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%,
                 // making the curves indistinguishable from a circle.
                 // Source: https://spencermortensen.com/articles/bezier-circle/
@@ -884,7 +868,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
 
                 break;
 
-            case 'line':
+            case GestureUtils.Gestures.Line:
                 if (Math.abs(firstx - lastx) < 10 && Math.abs(firsty - lasty) > 10) {
                     lastx = firstx;
                 }
@@ -897,7 +881,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 this._points.push({ X: lastx, Y: lasty });
                 this._points.push({ X: lastx, Y: lasty });
                 break;
-            case 'arrow':
+            case GestureUtils.Gestures.Arrow:
                 const x1 = left;
                 const y1 = top;
                 const x2 = right;
@@ -914,12 +898,11 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 this._points.push({ X: x3, Y: y3 });
                 this._points.push({ X: x4, Y: y4 });
                 this._points.push({ X: x2, Y: y2 });
-            // this._points.push({ X: x1, Y: y1 - 1 });
         }
-        return true;
+        return false;
     };
 
-    dispatchGesture = (gesture: 'box' | 'line' | 'startbracket' | 'endbracket' | 'stroke' | 'scribble' | 'text', stroke?: InkData, data?: any) => {
+    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => {
         const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
         return (
             target?.dispatchEvent(
@@ -1136,7 +1119,7 @@ ScriptingGlobals.add(function resetPen() {
 }, 'resets the pen tool');
 ScriptingGlobals.add(
     function createText(text: any, x: any, y: any) {
-        GestureOverlay.Instance.dispatchGesture('text', [{ X: x, Y: y }], text);
+        GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: x, Y: y }], text);
     },
     'creates a text document with inputted text and coordinates',
     '(text: any, x: any, y: any)'
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5d4e1c999..daf69d4f6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -568,6 +568,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
+            default:
+            case GestureUtils.Gestures.Circle:
+            case GestureUtils.Gestures.Rectangle:
+            case GestureUtils.Gestures.Triangle:
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
@@ -597,7 +601,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
-            case GestureUtils.Gestures.Box:
+            case GestureUtils.Gestures.Rectangle:
                 const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
                 const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
                 const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 6d1751b25..6eaf3c31a 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -11,6 +11,7 @@ import { InkTool } from '../../../../fields/InkField';
 import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { WebField } from '../../../../fields/URLField';
+import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
 import { aggregateBounds, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -85,6 +86,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     static SetShowLabels(show: boolean) {
         Doc.UserDoc()._showLabel = show;
     }
+    static GetRecognizeGestures() {
+        return BoolCast(Doc.UserDoc()._recognizeGestures);
+    }
+    static SetRecognizeGesturs(show: boolean) {
+        Doc.UserDoc()._recognizeGestures = show;
+    }
 
     // Determining UI Specs
     @computed get label() {
@@ -775,7 +782,7 @@ ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean)
     if (checkResult) {
         return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
     }
-    if (['circle', 'square', 'line'].includes(tool)) {
+    if ([GestureUtils.Gestures.Circle, GestureUtils.Gestures.Rectangle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Triangle].includes(tool as any)) {
         if (GestureOverlay.Instance.InkShape === tool) {
             Doc.ActiveTool = InkTool.None;
             GestureOverlay.Instance.InkShape = InkTool.None;
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 65f2bf80c..2d3b1fdb8 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -1,40 +1,34 @@
-import { Rect } from "react-measure";
-import { PointData } from "../fields/InkField";
-import { NDollarRecognizer } from "./ndollar";
+import { Rect } from 'react-measure';
+import { PointData } from '../fields/InkField';
+import { NDollarRecognizer } from './ndollar';
 
 export namespace GestureUtils {
     export class GestureEvent {
-        constructor(
-            readonly gesture: Gestures,
-            readonly points: PointData[],
-            readonly bounds: Rect,
-            readonly text?: any
-        ) { }
+        constructor(readonly gesture: Gestures, readonly points: PointData[], readonly bounds: Rect, readonly text?: any) {}
     }
 
-    export interface GestureEventDisposer { (): void; }
+    export interface GestureEventDisposer {
+        (): void;
+    }
 
-    export function MakeGestureTarget(
-        element: HTMLElement,
-        func: (e: Event, ge: GestureEvent) => void
-    ): GestureEventDisposer {
+    export function MakeGestureTarget(element: HTMLElement, func: (e: Event, ge: GestureEvent) => void): GestureEventDisposer {
         const handler = (e: Event) => func(e, (e as CustomEvent<GestureEvent>).detail);
-        element.addEventListener("dashOnGesture", handler);
-        return () => element.removeEventListener("dashOnGesture", handler);
+        element.addEventListener('dashOnGesture', handler);
+        return () => element.removeEventListener('dashOnGesture', handler);
     }
 
     export enum Gestures {
-        Box = "box",
-        Line = "line",
-        StartBracket = "startbracket",
-        EndBracket = "endbracket",
-        Stroke = "stroke",
-        Scribble = "scribble",
-        Text = "text",
-        Triangle = "triangle",
-        Circle = "circle",
-        Rectangle = "rectangle",
+        Line = 'line',
+        StartBracket = 'startbracket',
+        EndBracket = 'endbracket',
+        Stroke = 'stroke',
+        Scribble = 'scribble',
+        Text = 'text',
+        Triangle = 'triangle',
+        Circle = 'circle',
+        Rectangle = 'rectangle',
+        Arrow = 'arrow',
     }
 
     export const GestureRecognizer = new NDollarRecognizer(false);
-}
\ No newline at end of file
+}
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index ecd8df3e7..b10a9da17 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -1,4 +1,4 @@
-import { GestureUtils } from "./GestureUtils";
+import { GestureUtils } from './GestureUtils';
 
 /**
  * The $N Multistroke Recognizer (JavaScript version)
@@ -69,20 +69,20 @@ import { GestureUtils } from "./GestureUtils";
  * 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) { }
+    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) { }
+    constructor(public X: number, public Y: number, public Width: number, public Height: number) {}
 }
 
 //
@@ -113,8 +113,11 @@ export class Multistroke {
     public NumStrokes: number;
     public Unistrokes: Unistroke[];
 
-    constructor(public Name: string, useBoundedRotationInvariance: boolean, strokes: any[]) // constructor
-    {
+    constructor(
+        public Name: string,
+        useBoundedRotationInvariance: boolean,
+        strokes: any[] // constructor
+    ) {
         this.NumStrokes = strokes.length; // number of individual strokes
 
         const order = new Array(strokes.length); // array of integer indices
@@ -136,13 +139,13 @@ export class Multistroke {
 // Result class
 //
 export class Result {
-    constructor(public Name: string, public Score: any, public Time: any) { }
+    constructor(public Name: string, public Score: any, public Time: any) {}
 }
 
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 7;
+const NumMultistrokes = 6;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -152,7 +155,7 @@ 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 StartAngleIndex = NumPoints / 8; // eighth of gesture length
 const AngleSimilarityThreshold = Deg2Rad(30.0);
 
 //
@@ -164,48 +167,68 @@ export class NDollarRecognizer {
     /**
      * @IMPORTANT - IF YOU'RE ADDING A NEW GESTURE, BE SURE TO INCREMENT THE NumMultiStrokes CONST RIGHT ABOVE THIS CLASS.
      */
-    constructor(useBoundedRotationInvariance: boolean) // constructor
-    {
+    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(
+        this.Multistrokes[0] = new Multistroke(
+            GestureUtils.Gestures.Rectangle,
+            useBoundedRotationInvariance,
             new Array(
-                new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
-                new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
-                new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
-                new Point(106, 146), //new Point(80, 150), new Point(50, 146),
-                new Point(30, 143))
-        ));
-        this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(
-            new Array(new Point(12, 347), new Point(119, 347))
-        ));
-        this.Multistrokes[2] = new Multistroke(GestureUtils.Gestures.StartBracket, useBoundedRotationInvariance, new Array(
-            // new Array(new Point(145, 20), new Point(30, 21), new Point(34, 150))
-            new Array(new Point(31, 25), new Point(145, 20), new Point(31, 25), new Point(34, 150))
-        ));
-        this.Multistrokes[3] = new Multistroke(GestureUtils.Gestures.EndBracket, useBoundedRotationInvariance, new Array(
-            // new Array(new Point(150, 21), new Point(149, 150), new Point(26, 152))
-            // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150))
-            new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100))
-        ));
-        this.Multistrokes[4] = new Multistroke(GestureUtils.Gestures.Triangle, useBoundedRotationInvariance, new Array(
-            new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100))
-        ));
-        this.Multistrokes[5] = new Multistroke(GestureUtils.Gestures.Circle, useBoundedRotationInvariance, new Array(
-            new Array(new Point(200, 250), new Point(240, 230), new Point(248, 210), new Point(248, 190), new Point(240, 170), new Point(200, 150), new Point(160, 170), new Point(151, 190), new Point(151, 210), new Point(160, 230), new Point(201, 250))
-        ));
-        this.Multistrokes[6] = new Multistroke(GestureUtils.Gestures.Rectangle, useBoundedRotationInvariance, new Array(
+                new Array(
+                    new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
+                    new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
+                    new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
+                    new Point(106, 146), //new Point(80, 150), new Point(50, 146),
+                    new Point(30, 143)
+                )
+            )
+        );
+        this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347))));
+        this.Multistrokes[2] = new Multistroke(
+            GestureUtils.Gestures.StartBracket,
+            useBoundedRotationInvariance,
             new Array(
-                new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
-                new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
-                new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
-                new Point(106, 146), //new Point(80, 150), new Point(50, 146),
-                new Point(30, 143),
-                new Point(29, 220))
-        ));
-
+                // new Array(new Point(145, 20), new Point(30, 21), new Point(34, 150))
+                new Array(new Point(31, 25), new Point(145, 20), new Point(31, 25), new Point(34, 150))
+            )
+        );
+        this.Multistrokes[3] = new Multistroke(
+            GestureUtils.Gestures.EndBracket,
+            useBoundedRotationInvariance,
+            new Array(
+                // new Array(new Point(150, 21), new Point(149, 150), new Point(26, 152))
+                // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150))
+                new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100))
+            )
+        );
+        this.Multistrokes[4] = new Multistroke(
+            GestureUtils.Gestures.Triangle, // equilateral
+            useBoundedRotationInvariance,
+            new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
+        );
+        this.Multistrokes[5] = new Multistroke(
+            GestureUtils.Gestures.Circle,
+            useBoundedRotationInvariance,
+            new Array(
+                new Array(
+                    new Point(200, 250),
+                    new Point(240, 230),
+                    new Point(248, 210),
+                    new Point(248, 190),
+                    new Point(240, 170),
+                    new Point(200, 150),
+                    new Point(160, 170),
+                    new Point(151, 190),
+                    new Point(151, 210),
+                    new Point(160, 230),
+                    new Point(201, 250)
+                )
+            )
+        );
         //
         // PREDEFINED STROKES
         //
@@ -281,23 +304,25 @@ export class NDollarRecognizer {
     Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => {
         const t0 = Date.now();
         const points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
-        const candidate = new Unistroke("", useBoundedRotationInvariance, points);
+        const candidate = new Unistroke('', useBoundedRotationInvariance, points);
 
         var u = -1;
         var b = +Infinity;
-        for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template
-        {
-            if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
-            {
-                for (const unistroke of this.Multistrokes[i].Unistrokes) // for each unistroke within this multistroke
-                {
-                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
-                    {
+        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 (const unistroke of this.Multistrokes[i].Unistrokes) {
+                    // for each unistroke within this multistroke
+                    if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) {
+                        // strokes start in the same direction
                         var d;
                         if (useProtractor) {
                             d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor
-                        }
-                        else {
+                        } else {
                             d = DistanceAtBestAngle(candidate.Points, unistroke, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
                         }
                         if (d < b) {
@@ -309,8 +334,8 @@ export class NDollarRecognizer {
             }
         }
         const t1 = Date.now();
-        return (u === -1) ? null : 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[]) => {
         this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
@@ -321,15 +346,14 @@ export class NDollarRecognizer {
             }
         }
         return num;
-    }
+    };
 
     DeleteUserGestures = () => {
         this.Multistrokes.length = NumMultistrokes; // clear any beyond the original set
         return NumMultistrokes;
-    }
+    };
 }
 
-
 //
 // Private helper functions from here on down
 //
@@ -339,11 +363,13 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
     } else {
         for (var i = 0; i < n; i++) {
             HeapPermute(n - 1, order, orders);
-            if (n % 2 === 1) { // swap 0, n-1
+            if (n % 2 === 1) {
+                // swap 0, n-1
                 const tmp = order[0];
                 order[0] = order[n - 1];
                 order[n - 1] = tmp;
-            } else { // swap i, n-1
+            } else {
+                // swap i, n-1
                 const tmp = order[i];
                 order[i] = order[n - 1];
                 order[n - 1] = tmp;
@@ -355,15 +381,18 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
 function MakeUnistrokes(strokes: any, orders: any) {
     const unistrokes = new Array(); // array of point arrays
     for (const order of orders) {
-        for (var b = 0; b < Math.pow(2, order.length); b++) // use b's bits for directions
-        {
+        for (
+            var b = 0;
+            b < Math.pow(2, order.length);
+            b++ // use b's bits for directions
+        ) {
             const unistroke = new Array(); // array of points
             for (var i = 0; i < order.length; i++) {
                 var pts;
-                if (((b >> i) & 1) === 1) {// is b's bit at index i on?
+                if (((b >> i) & 1) === 1) {
+                    // is b's bit at index i on?
                     pts = strokes[order[i]].slice().reverse(); // copy and reverse
-                }
-                else {
+                } else {
                     pts = strokes[order[i]].slice(); // copy
                 }
                 for (const point of pts) {
@@ -391,17 +420,17 @@ function Resample(points: any, n: any) {
     const newpoints = new Array(points[0]);
     for (var i = 1; i < points.length; i++) {
         const d = Distance(points[i - 1], points[i]);
-        if ((D + d) >= I) {
+        if (D + d >= I) {
             const qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
             const qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y);
             const q = new Point(qx, qy);
             newpoints[newpoints.length] = q; // append new point 'q'
             points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i
             D = 0.0;
-        }
-        else D += d;
+        } else D += d;
     }
-    if (newpoints.length === n - 1) {// sometimes we fall a rounding-error short of adding the last point, so add it if so
+    if (newpoints.length === n - 1) {
+        // sometimes we fall a rounding-error short of adding the last point, so add it if so
         newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y);
     }
     return newpoints;
@@ -410,8 +439,8 @@ function IndicativeAngle(points: any) {
     const c = Centroid(points);
     return Math.atan2(c.Y - points[0].Y, c.X - points[0].X);
 }
-function RotateBy(points: any, radians: any) // rotates points around centroid
-{
+function RotateBy(points: any, radians: any) {
+    // rotates points around centroid
     const c = Centroid(points);
     const cos = Math.cos(radians);
     const sin = Math.sin(radians);
@@ -423,8 +452,8 @@ function RotateBy(points: any, radians: any) // rotates points around centroid
     }
     return newpoints;
 }
-function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D
-{
+function ScaleDimTo(points: any, size: any, ratio1D: any) {
+    // scales bbox uniformly for 1D, non-uniformly for 2D
     const B = BoundingBox(points);
     const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
     const newpoints = new Array();
@@ -435,8 +464,8 @@ function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniform
     }
     return newpoints;
 }
-function TranslateTo(points: any, pt: any) // translates points' centroid
-{
+function TranslateTo(points: any, pt: any) {
+    // translates points' centroid
     const c = Centroid(points);
     const newpoints = new Array();
     for (const { X, Y } of points) {
@@ -446,8 +475,8 @@ function TranslateTo(points: any, pt: any) // translates points' centroid
     }
     return newpoints;
 }
-function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protractor
-{
+function Vectorize(points: any, useBoundedRotationInvariance: any) {
+    // for Protractor
     var cos = 1.0;
     var sin = 0.0;
     if (useBoundedRotationInvariance) {
@@ -471,8 +500,8 @@ function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protra
     }
     return vector;
 }
-function OptimalCosineDistance(v1: any, v2: any) // for Protractor
-{
+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) {
@@ -509,7 +538,8 @@ function DistanceAtAngle(points: any, T: any, radians: any) {
     return PathDistance(newpoints, T.Points);
 }
 function Centroid(points: any) {
-    var x = 0.0, y = 0.0;
+    var x = 0.0,
+        y = 0.0;
     for (const point of points) {
         x += point.X;
         y += point.Y;
@@ -519,7 +549,10 @@ function Centroid(points: any) {
     return new Point(x, y);
 }
 function BoundingBox(points: any) {
-    var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
+    var minX = +Infinity,
+        maxX = -Infinity,
+        minY = +Infinity,
+        maxY = -Infinity;
     for (const { X, Y } of points) {
         minX = Math.min(minX, X);
         minY = Math.min(minY, Y);
@@ -528,38 +561,41 @@ function BoundingBox(points: any) {
     }
     return new Rectangle(minX, minY, maxX - minX, maxY - minY);
 }
-function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths
-{
+function PathDistance(pts1: any, pts2: any) {
+    // average distance between corresponding points in two paths
     var d = 0.0;
-    for (var i = 0; i < pts1.length; i++) {// assumes pts1.length == pts2.length
+    for (var i = 0; i < pts1.length; i++) {
+        // assumes pts1.length == pts2.length
         d += Distance(pts1[i], pts2[i]);
     }
     return d / pts1.length;
 }
-function PathLength(points: any) // length traversed by a point path
-{
+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
-{
+function Distance(p1: any, p2: any) {
+    // distance between two points
     const dx = p2.X - p1.X;
     const dy = p2.Y - p1.Y;
     return Math.sqrt(dx * dx + dy * dy);
 }
-function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector
-{
+function CalcStartUnitVector(points: any, index: any) {
+    // start angle from points[0] to points[index] normalized as a unit vector
     const v = new Point(points[index]?.X - points[0]?.X, points[index]?.Y - points[0]?.Y);
     const len = Math.sqrt(v.X * v.X + v.Y * v.Y);
     return new Point(v.X / len, v.Y / len);
 }
-function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2
-{
-    const n = (v1.X * v2.X + v1.Y * v2.Y);
+function AngleBetweenUnitVectors(v1: any, v2: any) {
+    // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2
+    const n = v1.X * v2.X + v1.Y * v2.Y;
     const c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1]
     return Math.acos(c); // arc cosine of the vector dot product
 }
-function Deg2Rad(d: any) { return (d * Math.PI / 180.0); }
\ No newline at end of file
+function Deg2Rad(d: any) {
+    return (d * Math.PI) / 180.0;
+}
-- 
cgit v1.2.3-70-g09d2


From a81ab2e6f75681bc4fb3a7b49d2056144e396b94 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 19 Oct 2022 12:38:19 -0400
Subject: fixed erasing straight line stroke segments caused by intersections
 with other strokes. cleaned up more with gestures.  changed docView lock icon
 to be part of docDecorations.

---
 src/client/views/DocumentDecorations.scss          | 28 ++++++-
 src/client/views/DocumentDecorations.tsx           | 41 +++++++++-
 src/client/views/GestureOverlay.tsx                | 21 +++---
 src/client/views/StyleProvider.scss                | 18 ++---
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 58 ++++++--------
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/ImageBox.tsx                |  5 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 20 ++---
 src/pen-gestures/GestureUtils.ts                   |  2 -
 src/pen-gestures/ndollar.ts                        | 88 +++++++++-------------
 11 files changed, 148 insertions(+), 137 deletions(-)

(limited to 'src/client/views/nodes/button/FontIconBox.tsx')

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 5c8dd36cc..4e0b061a6 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,7 +1,7 @@
 @import 'global/globalCssVariables';
 
 $linkGap: 3px;
-$headerHeight: 20px;
+$headerHeight: 25px;
 $resizeHandler: 8px;
 
 .documentDecorations-Dark,
@@ -54,6 +54,8 @@ $resizeHandler: 8px;
         grid-column-end: 4;
         flex-direction: row;
         gap: 2px;
+        pointer-events: all;
+        cursor: move;
 
         .documentDecorations-openButton {
             display: flex;
@@ -236,8 +238,8 @@ $resizeHandler: 8px;
     .documentDecorations-borderRadius {
         position: absolute;
         border-radius: 100%;
-        left: 3px;
-        top: 23px;
+        left: 7px;
+        top: 27px;
         background: $medium-gray;
         height: 10;
         width: 10;
@@ -245,6 +247,21 @@ $resizeHandler: 8px;
         cursor: nwse-resize;
     }
 
+    .documentDecorations-lock {
+        position: absolute;
+        background: black;
+        right: 11;
+        top: 30px;
+        color: gray;
+        height: 14;
+        width: 14;
+        pointer-events: all;
+        margin: auto;
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+    }
+
     .documentDecorations-rotationPath {
         position: absolute;
         width: 100%;
@@ -260,6 +277,7 @@ $resizeHandler: 8px;
         cursor: nwse-resize;
         background: unset;
         opacity: 1;
+        transform: scale(2);
     }
 
     .documentDecorations-topLeftResizer {
@@ -314,6 +332,7 @@ $resizeHandler: 8px;
     .documentDecorations-topLeftResizer:hover,
     .documentDecorations-bottomRightResizer:hover {
         opacity: 1;
+        background: black;
     }
 
     .documentDecorations-bottomRightResizer {
@@ -325,6 +344,7 @@ $resizeHandler: 8px;
         cursor: nesw-resize;
         background: unset;
         opacity: 1;
+        transform: scale(2);
     }
 
     .documentDecorations-topRightResizer {
@@ -339,7 +359,6 @@ $resizeHandler: 8px;
 
     .documentDecorations-topRightResizer:hover,
     .documentDecorations-bottomLeftResizer:hover {
-        cursor: nesw-resize;
         background: black;
         opacity: 1;
     }
@@ -392,6 +411,7 @@ $resizeHandler: 8px;
     justify-content: center;
     align-items: center;
     gap: 5px;
+    top: 4px;
     background: $light-gray;
 }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9881ef9f1..6cf7df357 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -140,6 +140,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         }
     };
 
+    @action onContainerDown = (e: React.PointerEvent): void => {
+        setupMoveUpEvents(
+            this,
+            e,
+            e => this.onBackgroundMove(true, e),
+            e => {},
+            emptyFunction
+        );
+    };
+
     @action onTitleDown = (e: React.PointerEvent): void => {
         setupMoveUpEvents(
             this,
@@ -311,6 +321,27 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
     };
 
+    @action
+    onLockDown = (e: React.PointerEvent): void => {
+        // Call util move event function
+        setupMoveUpEvents(
+            this, // target
+            e, // pointerEvent
+            returnFalse, // moveEvent
+            emptyFunction, // upEvent
+            e => {
+                UndoManager.RunInBatch(
+                    () =>
+                        SelectionManager.Views().map(dv => {
+                            dv.rootDoc._lockedPosition = !dv.rootDoc._lockedPosition;
+                            dv.rootDoc._pointerEvents = dv.rootDoc._lockedPosition ? 'none' : undefined;
+                        }),
+                    'toggleBackground'
+                );
+            } // clickEvent
+        );
+    };
+
     setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
         const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]);
         const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
@@ -791,7 +822,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            <div className="documentDecorations-topbar">
+                            <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}>
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
                                 {hideResizers ? <div /> : titleArea}
@@ -817,21 +848,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     key="rad"
                                     style={{
                                         background: `${this._isRounding ? Colors.MEDIUM_BLUE : undefined}`,
-                                        left: `${radiusHandleLocation + 3}`,
-                                        top: `${radiusHandleLocation + 23}`,
+                                        transform: `translate(${radiusHandleLocation}px, ${radiusHandleLocation}px)`,
                                     }}
                                     className={`documentDecorations-borderRadius`}
                                     onPointerDown={this.onRadiusDown}
                                     onContextMenu={e => e.preventDefault()}
                                 />
                             )}
+                            <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
+                                <FontAwesomeIcon size="sm" icon="lock" />
+                            </div>
 
                             {hideDocumentButtonBar ? null : (
                                 <div
                                     className="link-button-container"
                                     key="links"
                                     style={{
-                                        transform: ` translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+                                        transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
                                     }}>
                                     <DocumentButtonBar views={SelectionManager.Views} />
                                 </div>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 23b03bc50..362aa3c86 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,7 @@
 import React = require('react');
 import * as fitCurve from 'fit-curve';
 import { action, computed, observable, runInAction, trace } from 'mobx';
-import { Doc } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
@@ -49,7 +49,7 @@ interface GestureOverlayProps {
 export class GestureOverlay extends Touchable<GestureOverlayProps> {
     static Instance: GestureOverlay;
 
-    @observable public InkShape: string = '';
+    @observable public InkShape: Opt<GestureUtils.Gestures>;
     @observable public SavedColor?: string;
     @observable public SavedWidth?: number;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
@@ -644,8 +644,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
-            //push first points to so interactionUtil knows pointer is up
-            this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
 
             const initialPoint = this._points[0];
             const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
@@ -685,10 +683,10 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             //if any of the shape is activated in the CollectionFreeFormViewChrome
             else if (this.InkShape) {
                 this.makeBezierPolygon(this.InkShape, false);
-                this.dispatchGesture(GestureUtils.Gestures.Stroke);
+                this.dispatchGesture(this.InkShape);
                 this._points.length = 0;
                 if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
-                    this.InkShape = '';
+                    this.InkShape = undefined;
                     Doc.ActiveTool = InkTool.None;
                 }
             }
@@ -703,12 +701,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                         case GestureUtils.Gestures.Rectangle:
                         case GestureUtils.Gestures.Circle:
                             this.makeBezierPolygon(result.Name, true);
-                        case GestureUtils.Gestures.StartBracket:
-                        case GestureUtils.Gestures.EndBracket:
                             actionPerformed = this.dispatchGesture(result.Name);
                             break;
                         case GestureUtils.Gestures.Line:
-                            actionPerformed = this.handleLineGesture();
+                            if (!(actionPerformed = this.handleLineGesture())) {
+                                this.makeBezierPolygon(result.Name, true);
+                                actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Stroke);
+                            }
                             break;
                         case GestureUtils.Gestures.Scribble:
                             console.log('scribble');
@@ -972,7 +971,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                                 ActiveDash(),
                                 1,
                                 1,
-                                this.InkShape,
+                                this.InkShape ?? '',
                                 'none',
                                 1.0,
                                 false
@@ -999,7 +998,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                             ActiveDash(),
                             1,
                             1,
-                            this.InkShape,
+                            this.InkShape ?? '',
                             'none',
                             1.0,
                             false
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 8929954c8..b1c97164a 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -1,11 +1,11 @@
 .styleProvider-lock {
-    font-size: 12px;
-    width: 20; 
-    height: 20; 
-    position: absolute; 
-    right: -25; 
-    top: -5;
-    background: transparent;
+    font-size: 10;
+    width: 15;
+    height: 15;
+    position: absolute;
+    right: -0;
+    top: 0;
+    background: black;
     pointer-events: all;
     opacity: 0.3;
     display: flex;
@@ -15,7 +15,7 @@
     cursor: default;
 }
 .styleProvider-lock:hover {
-    opacity:1;
+    opacity: 1;
 }
 
 .styleProvider-treeView-icon,
@@ -26,4 +26,4 @@
 
 .styleProvider-treeView-icon {
     opacity: 0;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index aadcd7169..a5a886f42 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -276,7 +276,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Decorations:
             if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
                 return doc &&
-                    (isBackground() || selected) &&
+                    isBackground() &&
                     !Doc.IsSystem(doc) &&
                     (props?.renderDepth || 0) > 0 &&
                     ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index daf69d4f6..983cf4f9b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -568,6 +568,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
+            case GestureUtils.Gestures.Line:
+                break;
             default:
             case GestureUtils.Gestures.Circle:
             case GestureUtils.Gestures.Rectangle:
@@ -602,33 +604,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 e.stopPropagation();
                 break;
             case GestureUtils.Gestures.Rectangle:
-                const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
-                const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
-                const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
-                const bWidth = bounds.r - bounds.x;
-                const bHeight = bounds.b - bounds.y;
-                const sel = this.getActiveDocuments().filter(doc => {
-                    const l = NumCast(doc.x);
-                    const r = l + doc[WidthSym]();
-                    const t = NumCast(doc.y);
-                    const b = t + doc[HeightSym]();
-                    const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
-                    if (pass) {
-                        doc.x = l - bounds.x - bWidth / 2;
-                        doc.y = t - bounds.y - bHeight / 2;
-                    }
-                    return pass;
-                });
-                this.addDocument(Docs.Create.FreeformDocument(sel, { title: 'nested collection', x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
-                sel.forEach(d => this.props.removeDocument?.(d));
-                e.stopPropagation();
-                break;
-            case GestureUtils.Gestures.StartBracket:
-                const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
-                this._inkToTextStartX = start[0];
-                this._inkToTextStartY = start[1];
-                break;
-            case GestureUtils.Gestures.EndBracket:
                 if (this._inkToTextStartX && this._inkToTextStartY) {
                     const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
                     const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color);
@@ -805,15 +780,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
                 const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
                 for (var i = 0; i < inkData.length - 3; i += 4) {
-                    const intersects = Array.from(
-                        new Set(
-                            InkField.Segment(inkData, i).intersects({
-                                // compute all unique intersections
-                                p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
-                                p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
-                            }) as (number | string)[]
-                        )
-                    ); // convert to more manageable union array type
+                    const rawIntersects = InkField.Segment(inkData, i).intersects({
+                        // compute all unique intersections
+                        p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+                        p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
+                    });
+                    const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type
+                    if (intersects.length) {
+                        console.log();
+                    }
                     // return tuples of the inkingStroke intersected, and the t value of the intersection
                     intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
                 }
@@ -862,6 +837,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return segments;
     };
 
+    // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
+    // call in a test for linearity
+    bintersects = (curve: Bezier, otherCurve: Bezier) => {
+        if ((otherCurve as any)._linear) {
+            return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
+        }
+        return curve.intersects(otherCurve);
+    };
+
     /**
      * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
      * ink strokes in the current collection.
@@ -886,7 +870,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
 
                     const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
-                    curve.intersects(otherCurve).forEach((val: string | number, i: number) => {
+                    this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => {
                         // Converting the Bezier.js Split type to a t-value number.
                         const t = +val.toString().split('/')[0];
                         if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1c48d47e9..040e03150 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1082,7 +1082,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     <DocumentLinksButton
                         View={this.props.DocumentView()}
                         scaling={this.linkButtonInverseScaling}
-                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]}
+                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -32]}
                     />
                 )}
                 {audioView}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 959c641a8..461d6984d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { createSchema } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -77,10 +77,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
             { fireImmediately: true, delay: 1000 }
         );
+        const layoutDoc = this.layoutDoc;
         this._disposers.path = reaction(
             () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
             ({ nativeSize, width }) => {
-                if (true || !this.layoutDoc._height) {
+                if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 6eaf3c31a..fd0c0d141 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -771,36 +771,26 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
     CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
 }
 
-/** INK
- *      setActiveTool
- *      setStrokeWidth
- *      setStrokeColor
- **/
-
-ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setActiveTool(tool: InkTool | GestureUtils.Gestures, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
     if (checkResult) {
         return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
     }
-    if ([GestureUtils.Gestures.Circle, GestureUtils.Gestures.Rectangle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Triangle].includes(tool as any)) {
+    if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
         if (GestureOverlay.Instance.InkShape === tool) {
             Doc.ActiveTool = InkTool.None;
-            GestureOverlay.Instance.InkShape = InkTool.None;
+            GestureOverlay.Instance.InkShape = undefined;
         } else {
             Doc.ActiveTool = InkTool.Pen;
-            GestureOverlay.Instance.InkShape = tool;
+            GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
         }
     } else if (tool) {
         // pen or eraser
         if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
             Doc.ActiveTool = InkTool.None;
-        } else if (tool == InkTool.Write) {
-            // console.log("write mode selected - create groupDoc here!", tool)
-            Doc.ActiveTool = tool;
-            GestureOverlay.Instance.InkShape = '';
         } else {
             Doc.ActiveTool = tool as any;
-            GestureOverlay.Instance.InkShape = '';
+            GestureOverlay.Instance.InkShape = undefined;
         }
     } else {
         Doc.ActiveTool = InkTool.None;
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 2d3b1fdb8..41917aac9 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -19,8 +19,6 @@ export namespace GestureUtils {
 
     export enum Gestures {
         Line = 'line',
-        StartBracket = 'startbracket',
-        EndBracket = 'endbracket',
         Stroke = 'stroke',
         Scribble = 'scribble',
         Text = 'text',
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index b10a9da17..3ee9506cb 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -145,7 +145,7 @@ export class Result {
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 6;
+let NumMultistrokes = 0;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -162,73 +162,59 @@ const AngleSimilarityThreshold = Deg2Rad(30.0);
 // NDollarRecognizer class
 //
 export class NDollarRecognizer {
-    public Multistrokes: Multistroke[];
+    public Multistrokes: Multistroke[] = [];
 
-    /**
-     * @IMPORTANT - IF YOU'RE ADDING A NEW GESTURE, BE SURE TO INCREMENT THE NumMultiStrokes CONST RIGHT ABOVE THIS CLASS.
-     */
     constructor(
         useBoundedRotationInvariance: boolean // constructor
     ) {
         //
         // one predefined multistroke for each multistroke type
         //
-        this.Multistrokes = new Array(NumMultistrokes);
-        this.Multistrokes[0] = new Multistroke(
-            GestureUtils.Gestures.Rectangle,
-            useBoundedRotationInvariance,
-            new Array(
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Rectangle,
+                useBoundedRotationInvariance,
                 new Array(
-                    new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
-                    new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
-                    new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
-                    new Point(106, 146), //new Point(80, 150), new Point(50, 146),
-                    new Point(30, 143)
+                    new Array(
+                        new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
+                        new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
+                        new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
+                        new Point(106, 146), //new Point(80, 150), new Point(50, 146),
+                        new Point(30, 143)
+                    )
                 )
             )
         );
-        this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347))));
-        this.Multistrokes[2] = new Multistroke(
-            GestureUtils.Gestures.StartBracket,
-            useBoundedRotationInvariance,
-            new Array(
-                // new Array(new Point(145, 20), new Point(30, 21), new Point(34, 150))
-                new Array(new Point(31, 25), new Point(145, 20), new Point(31, 25), new Point(34, 150))
+        this.Multistrokes.push(new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))));
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Triangle, // equilateral
+                useBoundedRotationInvariance,
+                new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
             )
         );
-        this.Multistrokes[3] = new Multistroke(
-            GestureUtils.Gestures.EndBracket,
-            useBoundedRotationInvariance,
-            new Array(
-                // new Array(new Point(150, 21), new Point(149, 150), new Point(26, 152))
-                // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150))
-                new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100))
-            )
-        );
-        this.Multistrokes[4] = new Multistroke(
-            GestureUtils.Gestures.Triangle, // equilateral
-            useBoundedRotationInvariance,
-            new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
-        );
-        this.Multistrokes[5] = new Multistroke(
-            GestureUtils.Gestures.Circle,
-            useBoundedRotationInvariance,
-            new Array(
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Circle,
+                useBoundedRotationInvariance,
                 new Array(
-                    new Point(200, 250),
-                    new Point(240, 230),
-                    new Point(248, 210),
-                    new Point(248, 190),
-                    new Point(240, 170),
-                    new Point(200, 150),
-                    new Point(160, 170),
-                    new Point(151, 190),
-                    new Point(151, 210),
-                    new Point(160, 230),
-                    new Point(201, 250)
+                    new Array(
+                        new Point(200, 250),
+                        new Point(240, 230),
+                        new Point(248, 210),
+                        new Point(248, 190),
+                        new Point(240, 170),
+                        new Point(200, 150),
+                        new Point(160, 170),
+                        new Point(151, 190),
+                        new Point(151, 210),
+                        new Point(160, 230),
+                        new Point(201, 250)
+                    )
                 )
             )
         );
+        NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
         //
         // PREDEFINED STROKES
         //
-- 
cgit v1.2.3-70-g09d2


From 5df8e9041e7a0e8ac98f0c911a2b603c0d8e89d3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 20 Oct 2022 11:07:04 -0400
Subject: added double-tap to keep ink primitive mode.  cleaned up circle
 drawing and some other gesture code.

---
 src/client/util/CurrentUserUtils.ts                | 14 ++---
 src/client/util/InteractionUtils.tsx               |  6 +-
 src/client/views/GestureOverlay.tsx                | 69 +++++++---------------
 src/client/views/collections/CollectionMenu.tsx    |  7 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  3 +-
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 51 +++++++++-------
 7 files changed, 68 insertions(+), 84 deletions(-)

(limited to 'src/client/views/nodes/button/FontIconBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index bfa868dbc..2bc464127 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -46,7 +46,7 @@ interface Button {
     buttonText?: string;
 
     // fields that do not correspond to DocumentOption fields
-    scripts?: { script?: string; onClick?: string; }
+    scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
     funcs?: { [key:string]: string };
     subMenu?: Button[];
 }
@@ -628,13 +628,13 @@ export class CurrentUserUtils {
 
     static inkTools():Button[] {
         return [
-            { title: "Pen",    toolTip: "Pen (Ctrl+P)",          btnType: ButtonType.ToggleButton, icon: "pen-nib",                     scripts: {onClick:'{ return setActiveTool("pen", _readOnly_);}' }},
-            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} },
-            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
+            { title: "Pen",    toolTip: "Pen (Ctrl+P)",          btnType: ButtonType.ToggleButton, icon: "pen-nib",                     scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }},
+            { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
+            { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
             // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (Ctrl+Shift+L)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", _readOnly_);}`} },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
             { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
             { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 6622b498d..3cdf4dbd2 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -242,16 +242,16 @@ export namespace InteractionUtils {
                 const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
                 const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
                 const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
-                for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
+                for (var x = centerX - radius; x < centerX + radius; x++) {
                     const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
                     points.push({ X: x, Y: y });
                 }
-                for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
+                for (var x = centerX + radius; x > centerX - radius; x--) {
                     const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
                     const newY = centerY - (y - centerY);
                     points.push({ X: x, Y: newY });
                 }
-                points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
+                points.push({ X: centerX - radius, Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(-radius, 2)) + centerY });
                 break;
 
             case GestureUtils.Gestures.Line:
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 362aa3c86..a29073f14 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -16,7 +16,6 @@ import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { SelectionManager } from '../util/SelectionManager';
 import { Transform } from '../util/Transform';
-import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
 import './GestureOverlay.scss';
 import {
     ActiveArrowEnd,
@@ -53,6 +52,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     @observable public SavedColor?: string;
     @observable public SavedWidth?: number;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
+    @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode
 
     @observable private _thumbX?: number;
     @observable private _thumbY?: number;
@@ -616,29 +616,16 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         return false;
     };
 
-    handleLineGesture = (): boolean => {
-        const actionPerformed = false;
-        const B = this.svgBounds;
-
-        // get the two targets at the ends of the line
-        const ep1 = this._points[0];
-        const ep2 = this._points.lastElement();
-        const target1 = document.elementFromPoint(ep1.X, ep1.Y);
-        const target2 = document.elementFromPoint(ep2.X, ep2.Y);
-
-        const ge = new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
-            bubbles: true,
-            detail: {
-                points: this._points.slice(),
-                gesture: GestureUtils.Gestures.Line,
-                bounds: B,
-            },
-        });
-        target1?.dispatchEvent(ge);
-        target2?.dispatchEvent(ge);
-        return actionPerformed;
-    };
-
+    @action primCreated() {
+        if (!this.KeepPrimitiveMode) {
+            this.InkShape = undefined;
+            //get out of ink mode after each stroke=
+            //if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor);
+            Doc.ActiveTool = InkTool.None;
+            // SetActiveArrowStart('none');
+            // SetActiveArrowEnd('none');
+        }
+    }
     @action
     onPointerUp = (e: PointerEvent) => {
         if (this._points.length > 1) {
@@ -654,7 +641,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 switch (this.Tool) {
                     case ToolglassTools.InkToText:
                         this._strokes.push(this._points.slice());
-                        this._points.length = 0;
                         CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then(results => {
                             const wordResults = results.filter((r: any) => r.category === 'line');
                             const possibilities: string[] = [];
@@ -676,7 +662,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                         break;
                     case ToolglassTools.IgnoreGesture:
                         this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                        this._points.length = 0;
                         break;
                 }
             }
@@ -684,11 +669,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             else if (this.InkShape) {
                 this.makeBezierPolygon(this.InkShape, false);
                 this.dispatchGesture(this.InkShape);
-                this._points.length = 0;
-                if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
-                    this.InkShape = undefined;
-                    Doc.ActiveTool = InkTool.None;
-                }
+                this.primCreated();
             }
             // if we're not drawing in a toolglass try to recognize as gesture
             else {
@@ -697,18 +678,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 let actionPerformed = false;
                 if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
+                        case GestureUtils.Gestures.Line:
                         case GestureUtils.Gestures.Triangle:
                         case GestureUtils.Gestures.Rectangle:
                         case GestureUtils.Gestures.Circle:
                             this.makeBezierPolygon(result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
                             break;
-                        case GestureUtils.Gestures.Line:
-                            if (!(actionPerformed = this.handleLineGesture())) {
-                                this.makeBezierPolygon(result.Name, true);
-                                actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Stroke);
-                            }
-                            break;
                         case GestureUtils.Gestures.Scribble:
                             console.log('scribble');
                             break;
@@ -741,12 +717,9 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                     // TODO: nda - check inks to group here
                     checkInksToGroup();
                 }
-                this._points.length = 0;
             }
-        } else {
-            this._points.length = 0;
         }
-        CollectionFreeFormViewChrome.Instance?.primCreated();
+        this._points.length = 0;
     };
 
     makeBezierPolygon = (shape: string, gesture: boolean) => {
@@ -901,17 +874,17 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         return false;
     };
 
-    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => {
-        const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
+    dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, text?: any) => {
+        const points = (stroke ?? this._points).slice();
         return (
-            target?.dispatchEvent(
+            document.elementFromPoint(points[0].X, points[0].Y)?.dispatchEvent(
                 new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
                     bubbles: true,
                     detail: {
-                        points: stroke ?? this._points.slice(),
-                        gesture: gesture as any,
-                        bounds: this.getBounds(stroke ?? this._points),
-                        text: data,
+                        points,
+                        gesture,
+                        bounds: this.getBounds(points),
+                        text,
                     },
                 })
             ) || false
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 46e8494ab..db81f28f6 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -40,6 +40,7 @@ import { CollectionLinearView } from './collectionLinear';
 import './CollectionMenu.scss';
 import { COLLECTION_BORDER_WIDTH } from './CollectionView';
 import { TabDocView } from './TabDocView';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
 
 interface CollectionMenuProps {
     panelHeight: () => number;
@@ -767,7 +768,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
     private _draw = ['∿', '=', '⎯', '→', '↔︎', 'ロ', 'O'];
     private _head = ['', '', '', '', 'arrow', '', ''];
     private _end = ['', '', '', 'arrow', 'arrow', '', ''];
-    private _shapePrims = ['', '', 'line', 'line', 'line', 'rectangle', 'circle'];
+    private _shapePrims = ['', '', 'line', 'line', 'line', 'rectangle', 'circle'] as GestureUtils.Gestures[];
     private _title = ['pen', 'highlighter', 'line', 'line with arrow', 'line with double arrows', 'square', 'circle'];
     private _faName = ['pen-fancy', 'highlighter', 'minus', 'long-arrow-alt-right', 'arrows-alt-h', 'square', 'circle'];
     @observable _selectedPrimitive = this._shapePrims.length;
@@ -848,13 +849,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 SetActiveArrowEnd(this._end[i]);
                 SetActiveBezierApprox('300');
 
-                GestureOverlay.Instance.InkShape = this._shapePrims[i];
+                if (GestureOverlay.Instance) GestureOverlay.Instance.InkShape = this._shapePrims[i];
             } else {
                 this._selectedPrimitive = this._shapePrims.length;
                 Doc.ActiveTool = InkTool.None;
                 SetActiveArrowStart('');
                 SetActiveArrowEnd('');
-                GestureOverlay.Instance.InkShape = '';
+                if (GestureOverlay.Instance) GestureOverlay.Instance.InkShape = undefined;
                 SetActiveBezierApprox('0');
             }
             e.stopPropagation();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 983cf4f9b..7b93408c3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -568,9 +568,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
-            case GestureUtils.Gestures.Line:
-                break;
             default:
+            case GestureUtils.Gestures.Line:
             case GestureUtils.Gestures.Circle:
             case GestureUtils.Gestures.Rectangle:
             case GestureUtils.Gestures.Triangle:
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 040e03150..16b5809fa 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -655,7 +655,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         this._downX = e.clientX;
         this._downY = e.clientY;
-        if (Doc.ActiveTool === InkTool.None && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
+        if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
             // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
             if (
                 (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fd0c0d141..883c4460b 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -771,31 +771,42 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
     CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
 }
 
-ScriptingGlobals.add(function setActiveTool(tool: InkTool | GestureUtils.Gestures, checkResult?: boolean) {
+function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
     if (checkResult) {
-        return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
-    }
-    if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
-        if (GestureOverlay.Instance.InkShape === tool) {
-            Doc.ActiveTool = InkTool.None;
-            GestureOverlay.Instance.InkShape = undefined;
-        } else {
-            Doc.ActiveTool = InkTool.Pen;
-            GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+        return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
+            ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures)
+                ? Colors.MEDIUM_BLUE
+                : Colors.MEDIUM_BLUE_ALT
+            : 'transparent';
+    }
+    runInAction(() => {
+        if (GestureOverlay.Instance) {
+            GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
         }
-    } else if (tool) {
-        // pen or eraser
-        if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
-            Doc.ActiveTool = InkTool.None;
+        if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
+            if (GestureOverlay.Instance.InkShape === tool) {
+                Doc.ActiveTool = InkTool.None;
+                GestureOverlay.Instance.InkShape = undefined;
+            } else {
+                Doc.ActiveTool = InkTool.Pen;
+                GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+            }
+        } else if (tool) {
+            // pen or eraser
+            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+                Doc.ActiveTool = InkTool.None;
+            } else {
+                Doc.ActiveTool = tool as any;
+                GestureOverlay.Instance.InkShape = undefined;
+            }
         } else {
-            Doc.ActiveTool = tool as any;
-            GestureOverlay.Instance.InkShape = undefined;
+            Doc.ActiveTool = InkTool.None;
         }
-    } else {
-        Doc.ActiveTool = InkTool.None;
-    }
-});
+    });
+}
+
+ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
 
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) {
-- 
cgit v1.2.3-70-g09d2

