From 3381bbb0ef5160707513f4bbbe551ca551b64b0d Mon Sep 17 00:00:00 2001 From: bobzel 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 { 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 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(last - first + 1); // New parameter values + const uPrime = new Array(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(last - first + 1);// Parameterization + const u = new Array(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>(4); + const sz = 4; + const Vtemp = new Array>(4); for (var i = 0; i < 4; i++) Vtemp[i] = new Array(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(4), a2 = new Array(4); + const a1 = new Array(4), a2 = new Array(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(4), b2 = new Array(4); + const b1 = new Array(4), b2 = new Array(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(4), b2 = new Array(4); + const b1 = new Array(4), b2 = new Array(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(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); - var Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + const nPts = last - first + 1; // Number of pts in sub-curve + const Ax = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector A(nPts * 2); + const Ay = new Array(nPts * 2);// Precomputed rhs for eqn //std::vector 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(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(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 { @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 { } 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 { @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 { * 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 { 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 { } render() { - const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => void) => { strokeWidth={0} onPointerLeave={action(() => this._overStart = false)} onPointerEnter={action(() => this._overStart = true)} - onPointerDown={e => dragFunc(e)} + onPointerDown={dragFunc} pointerEvents="all" />; return ( - {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))} ); } 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 { * @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 { } 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 { 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"} /> )} {tangentLines.map((pts, i) => { const tangentLine = (x1: number, y1: number, x2: number, y2: number) => @@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component { 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 {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(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 { inkDoc.isInkMask = !inkDoc.isInkMask; inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; @@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent { - 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 { 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 + return
this._nearestScrPt = undefined)} @@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent 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 { @computed get controlPointsButton() { - const formatInstance = InkStrokeProperties.Instance; - return !formatInstance ? (null) :
+ return
{"Edit points"}
}> -
formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}> +
InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
- {formatInstance._lock ? "Unlock ratio" : "Lock ratio"}
}> -
formatInstance._lock = !formatInstance._lock)} > - + {InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}
}> +
InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} > +
{"Rotate 90˚"}
}> @@ -603,7 +604,7 @@ export class PropertiesView extends React.Component { 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 { 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 { 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, props: Opt { + 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 { 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 = new Set(); + const linkedDocSet = new Set(); 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 = new Map(); + const nextPageRanks = new Map(); this._results.forEach((_, doc) => { let nextPageRank = pageRankFromAll; @@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent (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 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 ( {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : @@ -115,12 +118,14 @@ export namespace InteractionUtils { } {arrowStart !== "arrow" ? (null) : - - + + } {arrowEnd !== "arrow" ? (null) : - - + + } } 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 {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)} ; }), this._points.length <= 1 ? (null) : {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)} ] ]; } 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 { 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 {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)} @@ -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 { @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 { 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 { @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 {
+
+
+
Arrow Scale:
+ {/*
{this.markScalInput}
*/} +
+ this.markScal = +e.target.value))} + onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("scale undo"); }} + onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }} + /> +
Arrow Head: