From 3381bbb0ef5160707513f4bbbe551ca551b64b0d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 13 Nov 2021 10:38:53 -0500
Subject: change isContentActive to a tri-state to allow turning on/off and
 default - fixes issues with videobox and others so that content can be turned
 off reliably.   added annotation overlay for treeViews for ppt like slides. 
 lots of fixes to tree view to get layout to be more robust.

---
 src/client/views/PropertiesView.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/PropertiesView.tsx')

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ab9022a84..1083e0075 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     freezeDimensions={true}
                     dontCenter={"y"}
                     isDocumentActive={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
                     NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
                     PanelWidth={panelWidth}
-- 
cgit v1.2.3-70-g09d2


From 91247d583e5e4c7205a1ed764dd0e3a12af3be25 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Nov 2021 23:37:49 -0500
Subject: fixed warnings/errors.  added inkingStroke comments.  need to
 double-click now to add a point to an ink stroke.

---
 src/client/util/bezierFit.ts                       | 242 ++++++++++-----------
 src/client/views/DocumentDecorations.tsx           |   8 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/InkControlPtHandles.tsx           |  34 ++-
 src/client/views/InkStroke.scss                    |   5 +-
 src/client/views/InkStrokeProperties.ts            |  15 +-
 src/client/views/InkTangentHandles.tsx             |  28 +--
 src/client/views/InkingStroke.tsx                  | 174 ++++++++-------
 src/client/views/MainView.tsx                      |   1 -
 src/client/views/PropertiesView.tsx                |  21 +-
 src/client/views/StyleProvider.tsx                 |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +-
 src/client/views/search/SearchBox.tsx              |  18 +-
 13 files changed, 269 insertions(+), 283 deletions(-)

(limited to 'src/client/views/PropertiesView.tsx')

diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 57c6dbbde..784bb2e18 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,5 +1,4 @@
 import { Point } from "../../pen-gestures/ndollar";
-import { max } from "lodash";
 
 class SmartRect {
     minx: number = 0;
@@ -21,45 +20,38 @@ class SmartRect {
 
     public ContainsPercentage(other: SmartRect, axis: Point) {
         var ret = 0;
-        var minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
-        var maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
-        ret = maxx > minx ? (maxx - minx) / (axis == new Point(1, 0) ? other.Width : other.Height) : 0;
+        const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
+        const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
+        ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0;
         return ret;
     }
     public static Bounds(p: Point[]) {
-        var r = new SmartRect();
+        const r = new SmartRect();
         if (p.length > 0) {
             r.minx = p[0].X; // These are the most likely to be extremal
             r.maxx = p.lastElement().X;
             r.miny = p[0].Y;
             r.maxy = p.lastElement().Y;
 
-            if (r.minx > r.maxx) {
-                var tmp = r.minx;
-                r.minx = r.maxx;
-                r.maxx = tmp;
-            }
-            if (r.miny > r.maxy) {
-                var tmp = r.miny;
-                r.miny = r.maxy;
-                r.maxy = tmp;
-            }
+            if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx];
+            if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny];
 
-            for (var pt of p) {
-                if (pt.X < r.minx)
+            for (const pt of p) {
+                if (pt.X < r.minx) {
                     r.minx = pt.X;
-                else if (pt.X > r.maxx)
+                } else if (pt.X > r.maxx) {
                     r.maxx = pt.X;
-
-                if (pt.Y < r.miny)
+                }
+                if (pt.Y < r.miny) {
                     r.miny = pt.Y;
-                else if (pt.Y > r.maxy)
+                } else if (pt.Y > r.maxy) {
                     r.maxy = pt.Y;
+                }
             }
         }
         return r;
     }
-};
+}
 
 function Normalize(p: Point) {
     const len = Math.sqrt(p.X * p.X + p.Y * p.Y);
@@ -67,7 +59,7 @@ function Normalize(p: Point) {
 }
 
 function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) {
-    var uPrime = new Array<number>(last - first + 1); //  New parameter values
+    const uPrime = new Array<number>(last - first + 1); //  New parameter values
 
     for (var i = first; i <= last; i++) {
         uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]);
@@ -76,36 +68,36 @@ function ReparameterizeBezier(d: Point[], first: number, last: number, u: number
 }
 function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) {
     var maxError = 0; //  Maximum error
-
     var splitPoint2D = (last - first + 1) / 2;
     for (var i = first + 1; i < last; i++) {
-        var P = [0, 0];  //  point on curve
+        const P = [0, 0];  //  point on curve
         EvalBezierFast(bezCurve, u[i - first], P);
-        var dx = P[0] - d[i].X;//  offset from point to curve
-        var dy = P[1] - d[i].Y;
-        var dist = Math.sqrt(dx * dx + dy * dy); //  Current error
+        const dx = P[0] - d[i].X;//  offset from point to curve
+        const dy = P[1] - d[i].Y;
+        const dist = Math.sqrt(dx * dx + dy * dy); //  Current error
         if (dist >= maxError) {
             maxError = dist;
-            if (splitPoint2D)
+            if (splitPoint2D) {
                 splitPoint2D = i;
+            }
         }
     }
     return { maxError, splitPoint2D };
 }
 function ChordLengthParameterize(d: Point[], first: number, last: number) {
-    var u = new Array<number>(last - first + 1);//  Parameterization
+    const u = new Array<number>(last - first + 1);//  Parameterization
 
     var prev = 0.0;
     u[0] = prev;
     for (var i = first + 1; i <= last; i++) {
-        var lastd = d[i - 1];
-        var curd = d[i];
-        var dx = lastd.X - curd.X;
-        var dy = lastd.Y - curd.Y;
+        const lastd = d[i - 1];
+        const curd = d[i];
+        const dx = lastd.X - curd.X;
+        const dy = lastd.Y - curd.Y;
         prev = u[i - first] = prev + Math.sqrt(dx * dx + dy * dy);
     }
 
-    var ulastfirst = u[last - first];
+    const ulastfirst = u[last - first];
     for (var i = first + 1; i <= last; i++) {
         u[i - first] /= ulastfirst;
     }
@@ -116,12 +108,12 @@ function ChordLengthParameterize(d: Point[], first: number, last: number) {
 *  B0, B1, B2, B3 :
 *  Bezier multipliers
 */
-function B0(u: number) { var tmp = 1.0 - u; return tmp * tmp * tmp; }
-function B1(u: number) { var tmp = 1.0 - u; return 3 * u * tmp * tmp; }
-function B2(u: number) { var tmp = 1.0 - u; return 3 * u * u * tmp; }
+function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; }
+function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; }
+function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; }
 function B3(u: number) { return u * u * u; }
 function bounds(p: Point[]) {
-    var r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
+    const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
 
     if (r.minx > r.maxx) (r.minx, r.maxx);
     if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max
@@ -137,10 +129,9 @@ function bounds(p: Point[]) {
 }
 
 
-
 function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
-    var sz = 4;
-    var Vtemp = new Array<Array<Point>>(4);
+    const sz = 4;
+    const Vtemp = new Array<Array<Point>>(4);
     for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4);
 
     /* Copy control points */
@@ -153,8 +144,8 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
     /* Triangle computation */
     for (var i = 1; i < sz; i++) {
         for (var j = 0; j < sz - i; j++) {
-            var a = Vtemp[i - 1][j];
-            var b = Vtemp[i - 1][j + 1];
+            const a = Vtemp[i - 1][j];
+            const b = Vtemp[i - 1][j + 1];
             Vtemp[i][j].X = b.X * t + a.X * (1 - t);
             Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t);  // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t);
         }
@@ -214,14 +205,14 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
 */
 function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) {
     if (deptha > 0) {
-        var a1 = new Array<Point>(4), a2 = new Array<Point>(4);
+        const a1 = new Array<Point>(4), a2 = new Array<Point>(4);
         splitCubic(a, 0.5, a1, a2);
-        var tmid = (t0 + t1) * 0.5;
+        const tmid = (t0 + t1) * 0.5;
         deptha--;
         if (depthb > 0) {
-            var b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
             splitCubic(b, 0.5, b1, b2);
-            var umid = (u0 + u1) * 0.5;
+            const umid = (u0 + u1) * 0.5;
             depthb--;
             if (SmartRect.Intersect(bounds(a1), bounds(b1))) {
                 recursively_intersect(a1, t0, tmid, deptha, b1, u0, umid, depthb, parameters);
@@ -247,9 +238,9 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
     }
     else {
         if (depthb > 0) {
-            var b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
             splitCubic(b, 0.5, b1, b2);
-            var umid = (u0 + u1) * 0.5;
+            const umid = (u0 + u1) * 0.5;
             depthb--;
             if (SmartRect.Intersect(bounds(a), bounds(b1))) {
                 recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters);
@@ -260,20 +251,20 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
         }
         else // Both segments are fully subdivided; now do line segments
         {
-            var xlk = a[3].X - a[0].X;
-            var ylk = a[3].Y - a[0].Y;
-            var xnm = b[3].X - b[0].X;
-            var ynm = b[3].Y - b[0].Y;
-            var xmk = b[0].X - a[0].X;
-            var ymk = b[0].Y - a[0].Y;
-            var det = xnm * ylk - ynm * xlk;
-            if (1.0 + det == 1.0) {
+            const xlk = a[3].X - a[0].X;
+            const ylk = a[3].Y - a[0].Y;
+            const xnm = b[3].X - b[0].X;
+            const ynm = b[3].Y - b[0].Y;
+            const xmk = b[0].X - a[0].X;
+            const ymk = b[0].Y - a[0].Y;
+            const det = xnm * ylk - ynm * xlk;
+            if (1.0 + det === 1.0) {
                 return;
             }
             else {
-                var detinv = 1.0 / det;
-                var s = (xnm * ymk - ynm * xmk) * detinv;
-                var t = (xlk * ymk - ylk * xmk) * detinv;
+                const detinv = 1.0 / det;
+                const s = (xnm * ymk - ynm * xmk) * detinv;
+                const t = (xlk * ymk - ylk * xmk) * detinv;
                 if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) {
                     return;
                 }
@@ -296,7 +287,7 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
         return;
     }
 
-    var Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
+    const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
 
     /* Copy array   */
     for (var i = 0; i <= degree; i++) {
@@ -317,14 +308,11 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
 }
 
 function EvalBezierFast(p: Point[], t: number, result: number[]) {
-    var n = 3;
-    var u: number, bc: number, tn: number, tmpX: number, tmpY: number;
-    u = 1.0 - t;
-    bc = 1;
-    tn = 1;
-
-    tmpX = p[0].X * u;
-    tmpY = p[0].Y * u;
+    const n = 3;
+    const u = 1.0 - t;
+    var bc = 1, tn = 1;
+    var tmpX = p[0].X * u;
+    var tmpY = p[0].Y * u;
     tn = tn * t;
     bc = bc * (n - 1 + 1) / 1;
     tmpX = (tmpX + tn * bc * p[1].X) * u;
@@ -342,22 +330,21 @@ function EvalBezierFast(p: Point[], t: number, result: number[]) {
 *Approximate unit tangents at endpoints and "center" of digitized curve
 */
 function ComputeLeftTangent(d: Point[], end: number) {
-    var use = 1;
-    var tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
+    const use = 1;
+    const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
     return Normalize(tHat1);
 }
 function ComputeRightTangent(d: Point[], end: number) {
-    var available = end;
-    var use = 1;
-    var tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y);
+    const use = 1;
+    const tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y);
     return Normalize(tHat2);
 }
 function ComputeCenterTangent(d: Point[], center: number) {
-    if (center == 0) {
+    if (center === 0) {
         return ComputeLeftTangent(d, center);
     }
-    var V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
-    var V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
+    const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
+    const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
     var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0);
     if (tHatCenter === new Point(0, 0)) {
         tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp();
@@ -365,15 +352,15 @@ function ComputeCenterTangent(d: Point[], center: number) {
     return Normalize(tHatCenter);
 }
 function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) {
-    var nPts = last - first + 1; // Number of pts in sub-curve
-    var Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
-    var Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const nPts = last - first + 1; // Number of pts in sub-curve
+    const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
 
     /* Compute the A's  */
     for (var i = 0; i < nPts; i++) {
-        var uprime = uPrime[i];
-        var b1 = B1(uprime);
-        var b2 = B2(uprime);
+        const uprime = uPrime[i];
+        const b1 = B1(uprime);
+        const b2 = B2(uprime);
         Ax[i] = tHat1.X * b1;
         Ay[i] = tHat1.Y * b1;
         Ax[i + 1 * nPts] = tHat2.X * b2;
@@ -381,44 +368,41 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     }
 
     /* Create the C and X matrices  */
-    var C = [[0, 0], [0, 0]];
-    var df = d[first];
-    var dl = d[last];
+    const C = [[0, 0], [0, 0]];
+    const df = d[first];
+    const dl = d[last];
 
-    var X = [0, 0];    // Matrix X
+    const X = [0, 0];    // Matrix X
     for (var i = 0; i < nPts; i++) {
         C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i];     //A[i+0*nPts].Dot(A[i+0*nPts]);
         C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]);
         C[1][0] = C[0][1];
         C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]);
-        var uprime = uPrime[i];
-        var b0plb1 = B0(uprime) + B1(uprime);
-        var b2plb3 = B2(uprime) + B3(uprime);
-        var df1 = d[first + i];
-        var tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
-        var tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
+        const uprime = uPrime[i];
+        const b0plb1 = B0(uprime) + B1(uprime);
+        const b2plb3 = B2(uprime) + B3(uprime);
+        const df1 = d[first + i];
+        const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
+        const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
 
         X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp)
         X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp)
     }
 
     /* Compute the determinants of C and X  */
-    var det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
-    var det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
-    var det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
+    const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12;
+    const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
+    const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
 
     /* Finally, derive alpha values */
-    if (det_C0_C1 == 0.0) {
-        det_C0_C1 = (C[0][0] * C[1][1]) * 10e-12;
-    }
-    var alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1;
-    var alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1;
+    var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1;
+    var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1;
 
     /* If alpha negative, use the Wu/Barsky heuristic (see text) */
     /* (if alpha is 0, you get coincident control points that lead to
     * divide by zero in any subsequent NewtonRaphsonRootFind() call. */
-    var segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
-    var epsilon = 1.0e-6 * segLength;
+    const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
+    const epsilon = 1.0e-6 * segLength;
     if (alpha_l < epsilon || alpha_r < epsilon) {
         /* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */
         alpha_l = alpha_r = segLength / 3.0;
@@ -432,15 +416,15 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     result[3] = dl;
     result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l));
     result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r));
-
 }
+
 /*
  *  NewtonRaphsonRootFind :
  *  Use Newton-Raphson iteration to find better root.
  */
 function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
-    var Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
-    var Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
+    const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
+    const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
 
     /* Compute Q(u) */
     var uPrime: number; //  Improved u
@@ -463,25 +447,24 @@ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
     EvalBezier(Q2, 1, u, Q2_u);
 
     /* Compute f(u)/f'(u) */
-    var numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
-    var denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
-    if (denominator == 0.0)
+    const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
+    const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
+    if (denominator === 0.0) {
         uPrime = u;
-    else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
+    } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
 
     return uPrime;
 }
 function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) {
-    var bezCurve = new Array<Point>(4);          //  Control points of fitted Bezier curve
-    var splitPoint2D: number;           //  Point2D to split point set at
-    var maxIterations = 4;      //  Max times to try iterating
+    const bezCurve = new Array<Point>(4);          //  Control points of fitted Bezier curve
+    const maxIterations = 4;      //  Max times to try iterating
 
-    var iterationError = error * error; //  Error below which you try iterating
-    var nPts = last - first + 1;        //  Number of points in subset
+    const iterationError = error * error; //  Error below which you try iterating
+    const nPts = last - first + 1;        //  Number of points in subset
 
     /*  Use heuristic if region only has two points in it */
-    if (nPts == 2) {
-        var dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3;
+    if (nPts === 2) {
+        const dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3;
 
         bezCurve[0] = d[first];
         bezCurve[3] = d[last];
@@ -499,7 +482,7 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve);
 
     /*  Find max deviation of points to fitted curve */
-    var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u);  //  Maximum fitting error
+    const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u);  //  Maximum fitting error
     if (maxError < Math.abs(error)) {
         result.push(bezCurve[1]);
         result.push(bezCurve[2]);
@@ -511,9 +494,9 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     /*  and iteration */
     if (maxError < iterationError) {
         for (var i = 0; i < maxIterations; i++) {
-            var uPrime = ReparameterizeBezier(d, first, last, u, bezCurve);   //  Improved parameter values
+            const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve);   //  Improved parameter values
             GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve);
-            var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, uPrime);
+            const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime);
             if (maxError < error) {
                 result.push(bezCurve[1]);
                 result.push(bezCurve[2]);
@@ -525,16 +508,15 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     }
 
     /* Fitting failed -- split at max error point and fit recursively */
-    var tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D);
+    const tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D);
     FitCubic(d, first, splitPoint2D, tHat1, tHatCenter, error, result);
-    var negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
+    const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
     FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result);
 }
 export function FitCurve(d: Point[], error: number) {
-    var tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
-    var tHat2 = ComputeRightTangent(d, d.length - 1);
-    var result: Point[] = [];
-    result.push(d[0]);
+    const tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
+    const tHat2 = ComputeRightTangent(d, d.length - 1);
+    const result = [d[0]];
     FitCubic(d, 0, d.length - 1, tHat1, tHat2, error, result);
     return result;
 }
@@ -543,14 +525,14 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
     tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1));
     tHat2 = new Point(-tHat2.X, -tHat2.Y);
     var u = ChordLengthParameterize(d, 0, d.length - 1);
-    var bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
+    const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
     GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls);    /*  Find max deviation of points to fitted curve */
     var finalCtrls = bezCurveCtrls.slice();
     var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u);
     for (var i = 0; i < 10; i++) {
-        var uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls);   //  Improved parameter values
+        const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls);   //  Improved parameter values
         GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls);
-        var { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
+        const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
         if (maxError < error) {
             error = maxError;
             finalCtrls = bezCurveCtrls.slice();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1c0b1b995..7d3959eba 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,9 +203,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
                 const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 };
                 const previousPoint = { X: e.clientX, Y: e.clientY };
                 const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
-                const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint);
+                const angle = InkStrokeProperties.Instance.angleChange(previousPoint, movedPoint, centerPoint);
                 const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
-                angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt);
+                angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, pt);
                 return false;
             },
             () => {
@@ -226,7 +226,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
         this._inkDragDocs = DragManager.docsBeingDragged
             .filter(doc => doc.type === DocumentType.INK)
             .map(doc => {
-                if (InkStrokeProperties.Instance?._lock) {
+                if (InkStrokeProperties.Instance._lock) {
                     Doc.SetNativeHeight(doc, NumCast(doc._height));
                     Doc.SetNativeWidth(doc, NumCast(doc._width));
                 }
@@ -249,7 +249,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
         const first = SelectionManager.Views()[0];
         let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
         var fixedAspect = Doc.NativeAspect(first.layoutDoc);
-        InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
+        InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
             .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
 
         const resizeHdl = this._resizeHdlId.split(" ")[0];
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 364bf05e2..d5e0ed962 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -112,7 +112,7 @@ export class KeyManager {
             case "escape":
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
-                InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false);
+                InkStrokeProperties.Instance._controlButton = false;
                 CurrentUserUtils.SelectedTool = InkTool.None;
                 var doDeselect = true;
                 if (SnappingManager.GetIsDragging()) {
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index a91e74c44..76ce73b0d 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -44,23 +44,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
         const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
-        if (InkStrokeProperties.Instance && ptFromScreen) {
+        if (ptFromScreen) {
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
             const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
             const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
-            const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
+            const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
                     return false;
                 }),
                 action(() => {
                     if (this.controlUndo) {
-                        InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex);
+                        InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
                     }
                     this.controlUndo?.end();
                     this.controlUndo = undefined;
@@ -75,11 +75,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
                                 if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                 if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
                         this.controlUndo?.end();
@@ -102,7 +102,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onDelete = (e: KeyboardEvent) => {
         if (["-", "Backspace", "Delete"].includes(e.key)) {
-            InkStrokeProperties.Instance?.deletePoints(this.props.inkView, e.shiftKey);
+            InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey);
             e.stopPropagation();
         }
     }
@@ -111,11 +111,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      * Changes the current selected control point.
      */
     @action
-    changeCurrPoint = (i: number) => {
-        if (InkStrokeProperties.Instance) {
-            InkStrokeProperties.Instance._currentPoint = i;
-        }
-    }
+    changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i
 
     render() {
         // Accessing the current ink's data and extracting all control points.
@@ -202,9 +198,9 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             const scaling = v2len / v1len;
             const v1n = { x: v1.x / v1len, y: v1.y / v1len };
             const v2n = { x: v2.x / v2len, y: v2.y / v2len };
-            const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y)
-            InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
-            InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
+            const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y);
+            InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
+            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
             return false;
         }, action(() => {
             this.controlUndo?.end();
@@ -214,7 +210,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
     }
 
     render() {
-        const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={"npt"}
+        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
             cx={pt.X}
             cy={pt.Y}
             r={this.props.screenSpaceLineWidth * 2}
@@ -223,12 +219,12 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             strokeWidth={0}
             onPointerLeave={action(() => this._overStart = false)}
             onPointerEnter={action(() => this._overStart = true)}
-            onPointerDown={e => dragFunc(e)}
+            onPointerDown={dragFunc}
             pointerEvents="all"
         />;
         return (<svg>
-            {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
-            {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
+            {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
+            {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
         </svg>
         );
     }
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 2127826b4..664f2448b 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -14,6 +14,9 @@
 }
 
 .inkStroke-wrapper {
+    display: flex;
+    align-items: center;
+    height: 100%;
     .inkStroke {
         mix-blend-mode: multiply;
         stroke-linejoin: round;
@@ -22,7 +25,7 @@
         transform-origin: top left;
         width: 100%;
         height: 100%;
-
+        pointer-events: none;
         svg:not(:root) {
             overflow: visible !important;
         }
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 4808bbc77..7ab631b03 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -15,14 +15,15 @@ import { InkingStroke } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 
 export class InkStrokeProperties {
-    static Instance: InkStrokeProperties | undefined;
+    static _Instance: InkStrokeProperties | undefined;
+    public static get Instance() { return this._Instance || new InkStrokeProperties(); }
 
     @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
 
     constructor() {
-        InkStrokeProperties.Instance = this;
+        InkStrokeProperties._Instance = this;
         reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None));
         reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
     }
@@ -150,15 +151,15 @@ export class InkStrokeProperties {
         } else {
             const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
             const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-            var samples: Point[] = [];
+            const samples: Point[] = [];
             var startDir = { x: 0, y: 0 };
             var endDir = { x: 0, y: 0 };
             for (var i = 0; i < splicedPoints.length / 4; i++) {
-                var bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
                 if (i === 0) startDir = bez.derivative(0);
                 if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
                 for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                    var pt = bez.compute(t);
+                    const pt = bez.compute(t);
                     samples.push(new Point(pt.x, pt.y));
                 }
             }
@@ -209,12 +210,12 @@ export class InkStrokeProperties {
             const ptFromScreen = view.ComponentView?.ptFromScreen;
             const ptToScreen = view.ComponentView?.ptToScreen;
             return !ptToScreen || !ptFromScreen ? ink :
-                ink.map(i => ptToScreen(i)).map(i => {
+                ink.map(ptToScreen).map(i => {
                     const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y };
                     const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling;
                     const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x);
                     const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x };
-                    const newpt = ptFromScreen!(newscrpt);
+                    const newpt = ptFromScreen(newscrpt);
                     return newpt;
                 });
         });
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index f88a20448..ab73e58a4 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -29,24 +29,23 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
      * @param handleNum The index of the currently selected handle point.
      */
     onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
-        if (InkStrokeProperties.Instance) {
-            var controlUndo: UndoManager.Batch | undefined;
-            const screenScale = this.props.ScreenToLocalTransform().Scale;
-            const order = handleIndex % 4;
-            const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
-            const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
-            const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
-            setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
+        var controlUndo: UndoManager.Batch | undefined;
+        const screenScale = this.props.ScreenToLocalTransform().Scale;
+        const order = handleIndex % 4;
+        const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
+        const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
+        const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
+        setupMoveUpEvents(this, e,
+            (e: PointerEvent, down: number[], delta: number[]) => {
                 if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
                 if (e.altKey) this.onBreakTangent(controlIndex);
-                InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+                InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
             }, () => {
                 controlUndo?.end();
                 UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
             }, emptyFunction
-            );
-        }
+        );
     }
 
     /**
@@ -66,9 +65,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
     }
 
     render() {
-        const formatInstance = InkStrokeProperties.Instance;
-        if (!formatInstance) return (null);
-
         // Accessing the current ink's data and extracting all handle points and handle lines.
         const data = this.props.screenCtrlPoints;
         const tangentHandles: HandlePoint[] = [];
@@ -107,7 +103,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             onPointerDown={e => this.onHandleDown(e, pts.I)}
                             pointerEvents="all"
                             cursor="default"
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
+                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />
                     </svg>)}
                 {tangentLines.map((pts, i) => {
                     const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
@@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             stroke={Colors.MEDIUM_BLUE}
                             strokeDasharray={"1 1"}
                             strokeWidth={1}
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />;
+                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />;
                     return <svg height="100" width="100" key={`line${i}`}>
                         {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
                         {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8ff080f81..0b3619b22 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,3 +1,25 @@
+/*
+    InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.
+
+    The primary data is:
+        data - an InkField which is an array of PointData (X,Y values).  The data is laid out as a sequence of simple bezier segments:
+              point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ...  (Note that segment endpoints are duplicated ie Point2 = Point 3)
+        brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
+        text - a text field that will be centered within a closed ink stroke
+        isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by 
+               the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
+
+    The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
+        Thus the mapping can roughly be described by:
+        the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
+        the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
+        NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space
+
+    InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in 
+        screen space (to avoid scaling artifacts)
+    
+    Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
+*/
 import React = require("react");
 import { action, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
@@ -30,35 +52,32 @@ const InkDocument = makeInterface(documentSchema);
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+    static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
-    static readonly MaskDim = 50000;
     public static IsClosed(inkData: InkData) {
         return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
-    @observable private _properties?: InkStrokeProperties;
-    _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
-    _selDisposer: IReactionDisposer | undefined;
+    private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
+    private _selDisposer?: IReactionDisposer;
 
-    @observable _nearestT: number | undefined;
-    @observable _nearestSeg: number | undefined;
-    @observable _nearestScrPt: { X: number, Y: number } | undefined;
-    @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
-
-    constructor(props: FieldViewProps & InkDocument) {
-        super(props);
-
-        this._properties = InkStrokeProperties.Instance;
-    }
+    @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
+    @observable _nearestT?: number;  // nearest t value within the nearest Bezier segment "
+    @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke ""
 
     componentDidMount() {
         this.props.setContentView?.(this);
         this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
-            selected => !selected && this.toggleControlButton());
+            selected => !selected && (InkStrokeProperties.Instance._controlButton = false));
     }
     componentWillUnmount() {
         this._selDisposer?.();
     }
 
+    /**
+    * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); 
+    * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
+    * the center of the ink stroke changes as the stroke is rotated.
+    */
     getCenter = (xf: Transform) => {
         const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
         const angle = -NumCast(this.layoutDoc.rotation);
@@ -80,6 +99,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
     }
 
+    /**
+     * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+     *    When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+     *  appear to illuminate what it covers up.  At the same time, all pixels that are not under the stroke will be
+     *  dimmed by a semi-opaque overlay mask.
+    */
     public static toggleMask = action((inkDoc: Doc) => {
         inkDoc.isInkMask = !inkDoc.isInkMask;
         inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
     });
     /**
-     * Handles the movement of the entire ink object when the user clicks and drags.
+     * Drags the a simple bezier segment of the stroke.
+     * Also adds a control point when double clicking on the stroke.
      */
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        const ptFromScreen = this.ptFromScreen;
         this._handledClick = false;
-        if (InkStrokeProperties.Instance && ptFromScreen) {
-            const inkView = this.props.docViewPath().lastElement();
-            const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
-            const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
-                (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
-                (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
-            const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
-            const controlIndex = nearestSeg;
-            const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
-            var controlUndo: UndoManager.Batch | undefined;
-            const isEditing = this._properties?._controlButton && this.props.isSelected();
-            setupMoveUpEvents(this, e,
-                !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
-                    const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
-                    const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
-                    InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
-                    return false;
-                }),
-                !isEditing ? returnFalse : action(() => {
-                    if (controlUndo) {
-                        InkStrokeProperties.Instance?.snapControl(inkView, controlIndex);
-                        InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3);
-                    }
-                    controlUndo?.end();
-                    controlUndo = undefined;
-                    UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
-                }),
-                action((e: PointerEvent, doubleTap: boolean | undefined) => {
-                    doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
-                    if (doubleTap && this._properties) {
-                        this._properties._controlButton = true;
-                        InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
-                        this._handledClick = true;  // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
-                    } else if (isEditing) {
-                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+        const inkView = this.props.docViewPath().lastElement();
+        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+        const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+            (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+            (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+        const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+        const controlIndex = nearestSeg;
+        const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+        var controlUndo: UndoManager.Batch | undefined;
+        const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+        setupMoveUpEvents(this, e,
+            !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
+                if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+                const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
+                const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
+                InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+                InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
+                return false;
+            }),
+            !isEditing ? returnFalse : action(() => {
+                controlUndo?.end();
+                controlUndo = undefined;
+                UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+            }),
+            action((e: PointerEvent, doubleTap: boolean | undefined) => {
+                doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
+                if (doubleTap) {
+                    InkStrokeProperties.Instance._controlButton = true;
+                    InkStrokeProperties.Instance._currentPoint = -1;
+                    this._handledClick = true;  // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+                    if (isEditing) {
+                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
                     }
-                }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)));
-        }
-    }
-
-    /**
-     * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
-     */
-    @action
-    toggleControlButton = () => {
-        if (!this.props.isSelected() && this._properties) {
-            this._properties._controlButton = false;
-        }
-    }
-
-    @action
-    checkHighlighter = () => {
-        if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) {
-            // this._previousColor = ActiveInkColor();
-            SetActiveInkColor("rgba(245, 230, 95, 0.75)");
-        }
+                }
+            }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)));
     }
 
     ptFromScreen = (scrPt: { X: number, Y: number }) => {
@@ -173,13 +175,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         return { X: scrPt[0], Y: scrPt[1] };
     }
 
+    /**
+    * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+    * @param scrPt - the point to snap to this stroke
+    * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+    *
+    * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+    */
     snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
         const { inkData } = this.inkScaledData();
-        const inkPt = this.ptFromScreen(scrPt);
-        const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+        const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
         return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
     }
 
+    /**
+    * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
+    * factor for converting between ink and screen space.
+    */
     inkScaledData = () => {
         const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
         const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -229,7 +241,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
         return SnappingManager.GetIsDragging() ? (null) :
-            !this._properties?._controlButton ?
+            !InkStrokeProperties.Instance._controlButton ?
                 (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
                     <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                         <InkEndPtHandles
@@ -287,15 +299,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             false, downHdlr);
         // Set of points rendered upon the ink that can be added if a user clicks on one.
 
-        return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
+        return <div className="inkStroke-wrapper">
             <svg className="inkStroke"
                 style={{
-                    width: "100%",
-                    height: "100%",
-                    pointerEvents: "none",
                     transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                     mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                    overflow: "visible",
                     cursor: this.props.isSelected() ? "default" : undefined
                 }}
                 onPointerLeave={action(e => this._nearestScrPt = undefined)}
@@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                     const cm = ContextMenu.Instance;
                     !Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
                     cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
-                    cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
+                    cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" });
                 }}
             >
                 {clickableLine(this.onPointerDown)}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 546b0e360..a9fea4a78 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -116,7 +116,6 @@ export class MainView extends React.Component {
             }, 0);
             setTimeout(() => ele.outerHTML = '', 1000);
         }
-        new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
             DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 1083e0075..18d5f1642 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -535,16 +535,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @computed
     get controlPointsButton() {
-        const formatInstance = InkStrokeProperties.Instance;
-        return !formatInstance ? (null) : <div className="inking-button">
+        return <div className="inking-button">
             <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}>
-                <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}>
+                <div className="inking-button-points"
+                    style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? "black" : "" }}
+                    onPointerDown={action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
                     <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
                 </div>
             </Tooltip>
-            <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
-                <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} >
-                    <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" />
+            <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
+                <div className="inking-button-lock" onPointerDown={action(() => InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} >
+                    <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? "lock" : "unlock"} color="white" size="lg" />
                 </div>
             </Tooltip>
             <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}>
@@ -603,7 +604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oldX = NumCast(this.selectedDoc?.x);
                 const oldY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10));
-                InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
+                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
                 const doc = this.selectedDoc;
                 if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
                     const ink = Cast(doc.data, InkField)?.inkData;
@@ -625,7 +626,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oX = NumCast(this.selectedDoc?.x);
                 const oY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10));
-                InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
+                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
                 const docu = this.selectedDoc;
                 if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
                     const ink = Cast(docu.data, InkField)?.inkData;
@@ -663,12 +664,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     set shapeWid(value) {
         const oldWidth = NumCast(this.selectedDoc?._width);
         this.selectedDoc && (this.selectedDoc._width = Number(value));
-        InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
+        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
     }
     set shapeHgt(value) {
         const oldHeight = NumCast(this.selectedDoc?._height);
         this.selectedDoc && (this.selectedDoc._height = Number(value));
-        InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
+        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
     }
 
     @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8d8630907..f09d532ad 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -93,7 +93,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
         case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
         case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
-        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily)));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
         case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
             StrCast(doc._showTitle,
                 props?.showTitle?.() ||
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9d0402075..72e84c141 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1146,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
                 } else if (!FormattedTextBox.LiveTextUndo) {
-                    selectAll(this._editorView!.state, (tr) => {
+                    selectAll(this._editorView.state, (tr) => {
                         this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
                     });
                 }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index fe297782c..09cfb2077 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -251,7 +251,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
         this._results.forEach((_, doc) => {
             this._pageRanks.set(doc, 1.0 / this._results.size);
 
-            if (Doc.GetProto(doc)[DirectLinksSym].size == 0) {
+            if (Doc.GetProto(doc)[DirectLinksSym].size === 0) {
                 this._linkedDocsOut.set(doc, new Set(this._results.keys()));
 
                 this._results.forEach((_, linkedDoc) => {
@@ -259,20 +259,20 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
                 });
             }
             else {
-                let linkedDocSet: Set<Doc> = new Set();
+                const linkedDocSet = new Set<Doc>();
 
                 Doc.GetProto(doc)[DirectLinksSym].forEach((link) => {
-                    let d1 = link?.anchor1 as Doc;
-                    let d2 = link?.anchor2 as Doc;
-                    if (doc == d1 && this._results.has(d2)) {
+                    const d1 = link?.anchor1 as Doc;
+                    const d2 = link?.anchor2 as Doc;
+                    if (doc === d1 && this._results.has(d2)) {
                         linkedDocSet.add(d2);
                         this._linkedDocsIn.get(d2)?.add(doc);
                     }
-                    else if (doc == d2 && this._results.has(d1)) {
+                    else if (doc === d2 && this._results.has(d1)) {
                         linkedDocSet.add(d1);
                         this._linkedDocsIn.get(d1)?.add(doc);
                     }
-                })
+                });
 
                 this._linkedDocsOut.set(doc, linkedDocSet);
             }
@@ -291,7 +291,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
         let converged = true;
         const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size;
 
-        let nextPageRanks: Map<Doc, number> = new Map<Doc, number>();
+        const nextPageRanks = new Map<Doc, number>();
 
         this._results.forEach((_, doc) => {
             let nextPageRank = pageRankFromAll;
@@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
 
         const isLinkSearch: boolean = this.props.linkSearch;
 
-        const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0))  // sorted by page rank
+        const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0));  // sorted by page rank
 
         const resultsJSX = Array();
 
-- 
cgit v1.2.3-70-g09d2


From c2cd77ca1d2a67539f0af2a68c1e7336b3bc232b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 Dec 2021 13:46:17 -0500
Subject: added scale factor for arrows.  added control point drag to
 reparameterize.  fixed toggling tangent lines to not happen when dragging.

---
 src/Utils.ts                             | 12 ++++----
 src/client/util/InteractionUtils.tsx     | 15 ++++++----
 src/client/util/bezierFit.ts             |  7 +++--
 src/client/views/GestureOverlay.tsx      |  8 ++---
 src/client/views/InkControlPtHandles.tsx |  3 +-
 src/client/views/InkStrokeProperties.ts  | 50 +++++++++++++++++++++++++++++---
 src/client/views/InkingStroke.tsx        | 11 ++++---
 src/client/views/PropertiesView.tsx      | 15 ++++++++++
 8 files changed, 95 insertions(+), 26 deletions(-)

(limited to 'src/client/views/PropertiesView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index ca1432de2..f2d9e7766 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -658,12 +658,6 @@ export function setupMoveUpEvents(
     (target as any)._lastTap = Date.now();
     (target as any)._downX = (target as any)._lastX = e.clientX;
     (target as any)._downY = (target as any)._lastY = e.clientY;
-    if (!(target as any)._doubleTime && noDoubleTapTimeout) {
-        (target as any)._doubleTime = setTimeout(() => {
-            noDoubleTapTimeout?.();
-            (target as any)._doubleTime = undefined;
-        }, doubleTapTimeout);
-    }
 
     const _moveEvent = (e: PointerEvent): void => {
         if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
@@ -685,6 +679,12 @@ export function setupMoveUpEvents(
         const isClick = Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4;
         upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY], isClick);
         if (isClick) {
+            if (!(target as any)._doubleTime && noDoubleTapTimeout) {
+                (target as any)._doubleTime = setTimeout(() => {
+                    noDoubleTapTimeout?.();
+                    (target as any)._doubleTime = undefined;
+                }, doubleTapTimeout);
+            }
             if ((target as any)._doubleTime && (target as any)._doubleTap) {
                 clearTimeout((target as any)._doubleTime);
                 (target as any)._doubleTime = undefined;
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4eb0be320..61872417b 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -91,7 +91,7 @@ export namespace InteractionUtils {
 
     export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
         color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
-        dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
+        markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
         downHdlr?: ((e: React.PointerEvent) => void)) {
         const pts = shape ? makePolygon(shape, points) : points;
 
@@ -108,6 +108,9 @@ export namespace InteractionUtils {
 
         const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
         const makerStrokeWidth = strokeWidth / 2;
+        const arrowWidthFactor = 3 * (markerScale ? markerScale : 0.5);// used to be 1.5
+        const arrowLengthFactor = 5 * (markerScale ? markerScale : 0.5);
+        const arrowNotchFactor = 2 * (markerScale ? markerScale : 0.5);
         return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
             {nodefs ? (null) : <defs>
                 {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
@@ -115,12 +118,14 @@ export namespace InteractionUtils {
                         <circle r={strokeWidth * 1.5} fill="context-stroke" />
                     </marker>}
                 {arrowStart !== "arrow" ? (null) :
-                    <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
-                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`${3 * makerStrokeWidth} ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth * 2} 0, ${3 * makerStrokeWidth} ${makerStrokeWidth * 1.5}, 0 0`} />
+                    <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+                            points={`${arrowLengthFactor * makerStrokeWidth} ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * makerStrokeWidth} ${makerStrokeWidth * arrowWidthFactor}, 0 0`} />
                     </marker>}
                 {arrowEnd !== "arrow" ? (null) :
-                    <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
-                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`0 ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth} 0, 0 ${makerStrokeWidth * 1.5}, ${3 * makerStrokeWidth} 0`} />
+                    <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+                        <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+                            points={`0 ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * arrowNotchFactor} 0, 0 ${makerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * makerStrokeWidth} 0`} />
                     </marker>}
             </defs>}
 
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 784bb2e18..8fc6de6f9 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -53,8 +53,11 @@ class SmartRect {
     }
 }
 
-function Normalize(p: Point) {
-    const len = Math.sqrt(p.X * p.X + p.Y * p.Y);
+export function Distance(p: Point) {
+    return Math.sqrt(p.X * p.X + p.Y * p.Y);
+}
+export function Normalize(p: Point) {
+    const len = Distance(p);
     return new Point(p.X / len, p.Y / len);
 }
 
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e2193c9ac..04abdbf37 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,7 @@ import * as fitCurve from 'fit-curve';
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc } from "../../fields/Doc";
-import { InkData, InkTool, PointData } from "../../fields/InkField";
+import { InkData, InkTool } from "../../fields/InkField";
 import { Cast, FieldValue, NumCast } from "../../fields/Types";
 import MobileInkOverlay from "../../mobile/MobileInkOverlay";
 import { GestureUtils } from "../../pen-gestures/GestureUtils";
@@ -18,7 +18,7 @@ import { SelectionManager } from "../util/SelectionManager";
 import { Transform } from "../util/Transform";
 import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
 import "./GestureOverlay.scss";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveArrowScale, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 import { RadialMenu } from "./nodes/RadialMenu";
 import HorizontalPalette from "./Palette";
@@ -850,14 +850,14 @@ export class GestureOverlay extends Touchable {
                 const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true);
                 return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
                     {InteractionUtils.CreatePolyline(l, b.left, b.top, strokeColor, width, width, "miter", "round",
-                        ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(),
+                        ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(),
                         ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
                 </svg>;
             }),
             this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height}
                 style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
                 {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "miter", "round", "",
-                    "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
+                    "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
             </svg>]
         ];
     }
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index ef457cac4..24f796105 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -51,12 +51,13 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
             const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
             const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
             const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+            const origInk = this.props.inkCtrlPoints.slice();
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
                     return false;
                 }),
                 action(() => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 695bdcc5a..cab4e1216 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,4 +1,5 @@
 import { Bezier } from "bezier-js";
+import { Normalize, Distance } from "../util/bezierFit";
 import { action, observable, reaction } from "mobx";
 import { Doc, NumListCast, Opt } from "../../fields/Doc";
 import { InkData, InkField, InkTool, PointData } from "../../fields/InkField";
@@ -225,14 +226,56 @@ export class InkStrokeProperties {
      */
     @undoBatch
     @action
-    moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) =>
+    moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
         this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
             const order = controlIndex % 4;
             const closed = InkingStroke.IsClosed(ink);
+            if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1) {
+                const cpt_before = ink[controlIndex];
+                const cpt = { X: cpt_before.X + deltaX, Y: cpt_before.Y + deltaY };
+                if (true) {
+                    const newink = origInk.slice();
+                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                    const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                    const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+                    const samplesLeft: Point[] = [];
+                    const samplesRight: Point[] = [];
+                    var startDir = { x: 0, y: 0 };
+                    var endDir = { x: 0, y: 0 };
+                    for (var i = 0; i < nearestSeg / 4 + 1; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === 0) startDir = bez.derivative(0);
+                        if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+                        for (var t = 0; t < (i === nearestSeg / 4 ? nearestT + .05 : 1); t += 0.05) {
+                            const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+                            samplesLeft.push(new Point(pt.x, pt.y));
+                        }
+                    }
+                    var { finalCtrls, error } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    for (var i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+                        for (var t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + .05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+                            const pt = bez.compute(Math.min(1, t));
+                            samplesRight.push(new Point(pt.x, pt.y));
+                        }
+                    }
+                    const { finalCtrls: rightCtrls, error: errorRight } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    finalCtrls = finalCtrls.concat(rightCtrls);
+                    newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                    return newink;
+                }
+            }
 
-            const newpts = ink.map((pt, i) => {
+            return ink.map((pt, i) => {
                 const leftHandlePoint = order === 0 && i === controlIndex + 1;
                 const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
+                if (controlIndex === i ||
+                    (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+                    (order === 3 && i === controlIndex - 1)) {
+                    return ({ X: pt.X + deltaX, Y: pt.Y + deltaY });
+                }
                 if (controlIndex === i ||
                     leftHandlePoint ||
                     rightHandlePoint ||
@@ -246,7 +289,6 @@ export class InkStrokeProperties {
                 }
                 return pt;
             });
-            return newpts;
         })
 
 
@@ -286,7 +328,7 @@ export class InkStrokeProperties {
                 if (snapData.distance < 10) {
                     const deltaX = (snapData.nearestPt.X - ink[controlIndex].X);
                     const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y);
-                    const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
+                    const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
                     console.log("X = " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
                     return res;
                 }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f5dd66949..5c7fc94bd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -31,7 +31,6 @@ import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
 import { TraceMobx } from "../../fields/util";
 import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils";
 import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
 import { SnappingManager } from "../util/SnappingManager";
 import { Transform } from "../util/Transform";
@@ -262,6 +261,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
         return SnappingManager.GetIsDragging() ? (null) :
             !InkStrokeProperties.Instance._controlButton ?
                 (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
@@ -275,7 +275,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                 <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                     {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
                         StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
-                        "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
+                        "none", startMarker, endMarker, markerScale, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
                     <InkControlPtHandles
                         inkView={this.props.docViewPath().lastElement()}
                         inkDoc={inkDoc}
@@ -299,6 +299,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
 
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+        const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
         const closed = InkingStroke.IsClosed(inkData);
         const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
         const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
@@ -307,7 +308,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth,
             StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
             StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
-            StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
+            markerScale, StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
         const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
         const highlightColor = !highlightIndex ?
             StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
@@ -317,7 +318,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
             StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
             StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
-            undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
+            markerScale, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
             false, downHdlr);
 
         return <div className="inkStroke-wrapper">
@@ -370,12 +371,14 @@ export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkP
 export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); }
 export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); }
 export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); }
+export function SetActiveArrowScale(value: number) { ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); }
 export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
 export function ActiveInkPen(): Doc { return Doc.UserDoc(); }
 export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
 export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
 export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
 export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
+export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); }
 export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
 export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); }
 export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 18d5f1642..8e2426006 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -756,6 +756,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; }
     @computed get unStrokd() { return this.selectedDoc?.color ? true : false; }
     @computed get widthStk() { return this.getField("strokeWidth") || "1"; }
+    @computed get markScal() { return Number(this.getField("strokeMakerScale") || "1"); }
     @computed get markHead() { return this.getField("strokeStartMarker") || ""; }
     @computed get markTail() { return this.getField("strokeEndMarker") || ""; }
     set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; }
@@ -763,6 +764,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         value && (this._lastDash = value) && (this.unStrokd = false);
         this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined);
     }
+    set markScal(value) { this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); }
     set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); }
     set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; }
     set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); }
@@ -770,6 +772,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
 
     @computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); }
+    @computed get markScaleInput() { return this.regInput("scale", this.markScal.toString(), (val: string) => this.markScal = Number(val)); }
 
 
     regInput = (key: string, value: any, setter: (val: string) => {}) => {
@@ -806,6 +809,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             </div>
 
             <div className="arrows">
+                <div className="arrows-head">
+                    <div className="width-top">
+                        <div className="width-title">Arrow Scale:</div>
+                        {/* <div className="width-input">{this.markScalInput}</div> */}
+                    </div>
+                    <input className="width-range" type="range"
+                        defaultValue={this.markScal} min={0} max={10}
+                        onChange={(action(e => this.markScal = +e.target.value))}
+                        onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("scale undo"); }}
+                        onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }}
+                    />
+                </div>
                 <div className="arrows-head">
                     <div className="arrows-head-title" >Arrow Head: </div>
                     <input key="markHead" className="arrows-head-input" type="checkbox"
-- 
cgit v1.2.3-70-g09d2

