import * as fitCurve from 'fit-curve'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../Utils'; import { Doc, Opt } from '../../fields/Doc'; import { InkData, InkTool } from '../../fields/InkField'; import { BoolCast, NumCast } from '../../fields/Types'; import MobileInkOverlay from '../../mobile/MobileInkOverlay'; import { GestureUtils } from '../../pen-gestures/GestureUtils'; import { MobileInkOverlayContent } from '../../server/Message'; import { InteractionUtils } from '../util/InteractionUtils'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { Transform } from '../util/Transform'; import './GestureOverlay.scss'; import { ActiveArrowEnd, ActiveArrowScale, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, } from './InkingStroke'; import { ObservableReactComponent } from './ObservableReactComponent'; import { checkInksToGroup } from './global/globalScripts'; import { DocumentView } from './nodes/DocumentView'; interface GestureOverlayProps { isActive: boolean; } @observer export class GestureOverlay extends ObservableReactComponent> { static Instance: GestureOverlay; static Instances: GestureOverlay[] = []; public static set RecognizeGestures(active) { Doc.UserDoc().recognizeGestures = active; } public static get RecognizeGestures() { return BoolCast(Doc.UserDoc().recognizeGestures); } @observable public InkShape: Opt = undefined; @observable public SavedColor?: string = undefined; @observable public SavedWidth?: number = undefined; @observable public Tool: ToolglassTools = ToolglassTools.None; @observable public KeepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode @observable private _thumbX?: number = undefined; @observable private _thumbY?: number = undefined; @observable private _selectedIndex: number = -1; @observable private _menuX: number = -300; @observable private _menuY: number = -300; @observable private _pointerY?: number = undefined; @observable private _points: { X: number; Y: number }[] = []; @observable private _strokes: InkData[] = []; @observable private _palette?: JSX.Element = undefined; @observable private _clipboardDoc?: JSX.Element = undefined; @observable private _possibilities: JSX.Element[] = []; public static DownDocView: DocumentView | undefined; @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100); } @computed private get showBounds() { return this.Tool !== ToolglassTools.None; } @observable private showMobileInkOverlay: boolean = false; private _overlayRef = React.createRef(); private _d1: Doc | undefined; private _inkToTextDoc: Doc | undefined; private thumbIdentifier?: number; private pointerIdentifier?: number; constructor(props: any) { super(props); makeObservable(this); GestureOverlay.Instances.push(this); } componentWillUnmount() { GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1); GestureOverlay.Instance = GestureOverlay.Instances.lastElement(); } componentDidMount() { GestureOverlay.Instance = this; } @action onPointerDown = (e: React.PointerEvent) => { if (!(e.target as any)?.className?.toString().startsWith('lm_')) { if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); } } }; @action onPointerMove = (e: PointerEvent) => { this._points.push({ X: e.clientX, Y: e.clientY }); if (this._points.length > 1) { const B = this.svgBounds; const initialPoint = this._points[0]; const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { switch (this.Tool) { case ToolglassTools.RadialMenu: return true; } } } return false; }; @action primCreated() { if (!this.KeepPrimitiveMode) { this.InkShape = undefined; //get out of ink mode after each stroke= //if (Doc.ActiveTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor); Doc.ActiveTool = InkTool.None; // SetActiveArrowStart('none'); // SetActiveArrowEnd('none'); } } @action onPointerUp = (e: PointerEvent) => { GestureOverlay.DownDocView = undefined; if (this._points.length > 1) { const B = this.svgBounds; const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); //if any of the shape is activated in the CollectionFreeFormViewChrome if (this.InkShape) { this.makeBezierPolygon(this.InkShape, false); this.dispatchGesture(this.InkShape); this.primCreated(); } // if we're not drawing in a toolglass try to recognize as gesture else { // need to decide when to turn gestures back on const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; if (GestureOverlay.RecognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case GestureUtils.Gestures.Line: case GestureUtils.Gestures.Triangle: case GestureUtils.Gestures.Rectangle: case GestureUtils.Gestures.Circle: this.makeBezierPolygon(result.Name, true); actionPerformed = this.dispatchGesture(result.Name); break; case GestureUtils.Gestures.Scribble: console.log('scribble'); break; } } // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document if (!actionPerformed) { const newPoints = this._points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); newPoints.pop(); const controlPoints: { X: number; Y: number }[] = []; const bezierCurves = (fitCurve as any)(newPoints, 10); for (const curve of bezierCurves) { controlPoints.push({ X: curve[0][0], Y: curve[0][1] }); controlPoints.push({ X: curve[1][0], Y: curve[1][1] }); controlPoints.push({ X: curve[2][0], Y: curve[2][1] }); controlPoints.push({ X: curve[3][0], Y: curve[3][1] }); } const dist = Math.sqrt( (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y) ); if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; this._points.length = 0; this._points.push(...controlPoints); this.dispatchGesture(GestureUtils.Gestures.Stroke); // TODO: nda - check inks to group here checkInksToGroup(); } } } this._points.length = 0; }; makeBezierPolygon = (shape: string, gesture: boolean) => { const xs = this._points.map(p => p.X); const ys = this._points.map(p => p.Y); var right = Math.max(...xs); var left = Math.min(...xs); var bottom = Math.max(...ys); var top = Math.min(...ys); const firstx = this._points[0].X; const firsty = this._points[0].Y; var lastx = this._points[this._points.length - 2].X; var lasty = this._points[this._points.length - 2].Y; var fourth = (lastx - firstx) / 4; if (isNaN(fourth) || fourth === 0) { fourth = 0.01; } var m = (lasty - firsty) / (lastx - firstx); if (isNaN(m) || m === 0) { m = 0.01; } const b = firsty - m * firstx; if (shape === 'noRec') { return false; } if (!gesture) { //if shape options is activated in inkOptionMenu //take second to last point because _point[length-1] is _points[0] right = this._points[this._points.length - 2].X; left = this._points[0].X; bottom = this._points[this._points.length - 2].Y; top = this._points[0].Y; if (shape !== GestureUtils.Gestures.Arrow && shape !== GestureUtils.Gestures.Line && shape !== GestureUtils.Gestures.Circle) { if (left > right) { const temp = right; right = left; left = temp; } if (top > bottom) { const temp = top; top = bottom; bottom = temp; } } } this._points.length = 0; switch (shape) { case GestureUtils.Gestures.Rectangle: this._points.push({ X: left, Y: top }); this._points.push({ X: left, Y: top }); this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: top }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: top }); this._points.push({ X: left, Y: top }); break; case GestureUtils.Gestures.Triangle: this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: right, Y: bottom }); this._points.push({ X: (right + left) / 2, Y: top }); this._points.push({ X: (right + left) / 2, Y: top }); this._points.push({ X: (right + left) / 2, Y: top }); this._points.push({ X: (right + left) / 2, Y: top }); this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); break; case GestureUtils.Gestures.Circle: // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%, // making the curves indistinguishable from a circle. // Source: https://spencermortensen.com/articles/bezier-circle/ const c = 0.551915024494; const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve. this._points.push({ X: centerX, Y: centerY + radius }); this._points.push({ X: centerX + c * radius, Y: centerY + radius }); this._points.push({ X: centerX + radius, Y: centerY + c * radius }); this._points.push({ X: centerX + radius, Y: centerY }); this._points.push({ X: centerX + radius, Y: centerY }); this._points.push({ X: centerX + radius, Y: centerY - c * radius }); this._points.push({ X: centerX + c * radius, Y: centerY - radius }); this._points.push({ X: centerX, Y: centerY - radius }); this._points.push({ X: centerX, Y: centerY - radius }); this._points.push({ X: centerX - c * radius, Y: centerY - radius }); this._points.push({ X: centerX - radius, Y: centerY - c * radius }); this._points.push({ X: centerX - radius, Y: centerY }); this._points.push({ X: centerX - radius, Y: centerY }); this._points.push({ X: centerX - radius, Y: centerY + c * radius }); this._points.push({ X: centerX - c * radius, Y: centerY + radius }); this._points.push({ X: centerX, Y: centerY + radius }); break; case GestureUtils.Gestures.Line: if (Math.abs(firstx - lastx) < 10 && Math.abs(firsty - lasty) > 10) { lastx = firstx; } if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) { lasty = firsty; } this._points.push({ X: firstx, Y: firsty }); this._points.push({ X: firstx, Y: firsty }); this._points.push({ X: lastx, Y: lasty }); this._points.push({ X: lastx, Y: lasty }); break; case GestureUtils.Gestures.Arrow: const x1 = left; const y1 = top; const x2 = right; const y2 = bottom; const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2)); const L2 = L1 / 5; const angle = 0.785398; const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle)); const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); this._points.push({ X: x1, Y: y1 }); this._points.push({ X: x2, Y: y2 }); this._points.push({ X: x3, Y: y3 }); this._points.push({ X: x4, Y: y4 }); this._points.push({ X: x2, Y: y2 }); } return false; }; dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, text?: any) => { const points = (stroke ?? this._points).slice(); return ( document.elementFromPoint(points[0].X, points[0].Y)?.dispatchEvent( new CustomEvent('dashOnGesture', { bubbles: true, detail: { points, gesture, bounds: GestureOverlay.getBounds(points), text, }, }) ) || false ); }; public static getBounds = (stroke: InkData, pad?: boolean) => { const padding = pad ? [-20000, 20000] : []; const xs = [...padding, ...stroke.map(p => p.X)]; const ys = [...padding, ...stroke.map(p => p.Y)]; const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); const top = Math.min(...ys); return { right, left, bottom, top, width: right - left, height: bottom - top }; }; @computed get svgBounds() { return GestureOverlay.getBounds(this._points); } get elements() { const selView = GestureOverlay.DownDocView; const width = Number(ActiveInkWidth()) * NumCast(selView?.Document._freeform_scale, 1); // * (selView?.screenToViewTransform().Scale || 1); const rect = this._overlayRef.current?.getBoundingClientRect(); const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true); B.left = B.left - width / 2; B.right = B.right + width / 2; B.top = B.top - width / 2 - (rect?.y || 0); B.bottom = B.bottom + width / 2; B.width += width; B.height += width; const fillColor = ActiveFillColor(); const strokeColor = fillColor && fillColor !== 'transparent' ? fillColor : ActiveInkColor(); return [ this.props.children, this._palette, [ this._strokes.map((l, i) => { 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(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape ?? '', 'none', 1.0, false )} ); }), this._points.length <= 1 ? null : ( {InteractionUtils.CreatePolyline( this._points.map(p => ({ X: p.X - (rect?.x || 0), Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, 'miter', 'round', '', 'none' /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape ?? '', 'none', 1.0, false )} ), ], ]; } screenToLocalTransform = () => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1); return300 = () => 300; @action public openFloatingDoc = (doc: Doc) => { this._clipboardDoc = ( ); }; @action public closeFloatingDoc = () => { this._clipboardDoc = undefined; }; @action enableMobileInkOverlay = (content: MobileInkOverlayContent) => { this.showMobileInkOverlay = content.enableOverlay; }; render() { return (
{this.showMobileInkOverlay ? : null} {this.elements}
{this._clipboardDoc}
); } } // export class export enum ToolglassTools { InkToText = 'inktotext', IgnoreGesture = 'ignoregesture', RadialMenu = 'radialmenu', None = 'none', } ScriptingGlobals.add('GestureOverlay', GestureOverlay); ScriptingGlobals.add(function setToolglass(tool: any) { runInAction(() => (GestureOverlay.Instance.Tool = tool)); }); ScriptingGlobals.add(function setPen(width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { runInAction(() => { GestureOverlay.Instance.SavedColor = ActiveInkColor(); SetActiveInkColor(color); GestureOverlay.Instance.SavedWidth = ActiveInkWidth(); SetActiveInkWidth(width); SetActiveFillColor(fill); SetActiveArrowStart(arrowStart); SetActiveArrowStart(arrowEnd); SetActiveDash(dash); }); }); ScriptingGlobals.add(function resetPen() { runInAction(() => { SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? 'rgb(0, 0, 0)'); SetActiveInkWidth(GestureOverlay.Instance.SavedWidth?.toString() ?? '2'); }); }, 'resets the pen tool'); ScriptingGlobals.add( function createText(text: any, x: any, y: any) { GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: x, Y: y }], text); }, 'creates a text document with inputted text and coordinates', '(text: any, x: any, y: any)' );