diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2020-04-10 22:16:16 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2020-04-10 22:16:16 -0400 |
commit | 5f52671adb7a3ba078230b11b4a03da4d84e37ff (patch) | |
tree | ec4fcb1c5d124e215a1fab90f649b6b2662e6c89 | |
parent | 5e93a7535b1d210bc20c57ea656257800bd4690b (diff) | |
parent | af7fb7bf59ae3a2984d2ea8d668757f75d66d63d (diff) |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
-rw-r--r-- | src/client/util/InteractionUtils.tsx | 22 | ||||
-rw-r--r-- | src/client/views/GestureOverlay.tsx | 194 | ||||
-rw-r--r-- | src/client/views/Touchable.tsx | 7 | ||||
-rw-r--r-- | src/pen-gestures/GestureUtils.ts | 8 | ||||
-rw-r--r-- | src/pen-gestures/ndollar.ts | 3 |
5 files changed, 132 insertions, 102 deletions
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index f2d569cf3..b1f136430 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -13,7 +13,6 @@ export namespace InteractionUtils { export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> { constructor( readonly fingers: number, - // readonly points: T extends React.TouchEvent ? React.TouchList : TouchList, readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[], @@ -23,6 +22,11 @@ export namespace InteractionUtils { export interface MultiTouchEventDisposer { (): void; } + /** + * + * @param element - element to turn into a touch target + * @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart) + */ export function MakeMultiTouchTarget( element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void @@ -48,6 +52,11 @@ export namespace InteractionUtils { }; } + /** + * Turns an element onto a target for touch hold handling. + * @param element - element to add events to + * @param func - function to add to the event + */ export function MakeHoldTouchTarget( element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void @@ -78,7 +87,6 @@ export namespace InteractionUtils { return myTouches; } - // TODO: find a way to reference this function from InkingStroke instead of copy pastign here. copied bc of weird error when on mobile view export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) { const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); return ( @@ -93,6 +101,11 @@ export namespace InteractionUtils { ); } + /** + * Returns whether or not the pointer event passed in is of the type passed in + * @param e - pointer event. this event could be from a mouse, a pen, or a finger + * @param type - InteractionUtils.(PENTYPE | ERASERTYPE | MOUSETYPE | TOUCHTYPE) + */ export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { switch (type) { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 @@ -105,6 +118,11 @@ export namespace InteractionUtils { } } + /** + * Returns euclidean distance between two points + * @param pt1 + * @param pt2 + */ export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number { return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2)); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 69aa8dbaa..1977f2406 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -82,6 +82,9 @@ export default class GestureOverlay extends Touchable { this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc)); } + /** + * Ignores all touch events that belong to a hand being held down. + */ getNewTouches(e: React.TouchEvent | TouchEvent) { const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches); const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches); @@ -121,6 +124,8 @@ export default class GestureOverlay extends Touchable { return; } + // this chunk adds new touch targets to a map of pointer events; this helps us keep track of individual fingers + // so that we can know, for example, if two fingers are pinching out or in. const actualPts: React.Touch[] = []; for (let i = 0; i < te.touches.length; i++) { const pt: any = te.touches.item(i); @@ -128,9 +133,6 @@ export default class GestureOverlay extends Touchable { // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) // and this seems to be the only way of differentiating pen and touch on touch events if (pt.radiusX > 1 && pt.radiusY > 1) { - // if (typeof pt.identifier !== "string") { - // pt.identifier = Utils.GenerateGuid(); - // } this.prevPoints.set(pt.identifier, pt); } } @@ -144,6 +146,7 @@ export default class GestureOverlay extends Touchable { ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); const nts = this.getNewTouches(te); + // if there are fewer than five touch events, handle as a touch event if (nts.nt.length < 5) { const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); target?.dispatchEvent( @@ -161,7 +164,7 @@ export default class GestureOverlay extends Touchable { ) ); if (nts.nt.length === 1) { - console.log("started"); + // -- radial menu code -- this._holdTimer = setTimeout(() => { console.log("hold"); const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); @@ -200,6 +203,7 @@ export default class GestureOverlay extends Touchable { document.addEventListener("touchmove", this.onReactTouchMove); document.addEventListener("touchend", this.onReactTouchEnd); } + // otherwise, handle as a hand event else { this.handleHandDown(te); document.removeEventListener("touchmove", this.onReactTouchMove); @@ -207,67 +211,6 @@ export default class GestureOverlay extends Touchable { } } - onReactHoldTouchMove = (e: TouchEvent) => { - document.removeEventListener("touchmove", this.onReactTouchMove); - document.removeEventListener("touchend", this.onReactTouchEnd); - document.removeEventListener("touchmove", this.onReactHoldTouchMove); - document.removeEventListener("touchend", this.onReactHoldTouchEnd); - document.addEventListener("touchmove", this.onReactHoldTouchMove); - document.addEventListener("touchend", this.onReactHoldTouchEnd); - const nts: any = this.getNewTouches(e); - if (this.prevPoints.size === 1 && this._holdTimer) { - clearTimeout(this._holdTimer); - } - document.dispatchEvent( - new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldMove", - { - bubbles: true, - detail: { - fingers: this.prevPoints.size, - targetTouches: nts.ntt, - touches: nts.nt, - changedTouches: nts.nct, - touchEvent: e - } - }) - ); - } - - onReactHoldTouchEnd = (e: TouchEvent) => { - const nts: any = this.getNewTouches(e); - if (this.prevPoints.size === 1 && this._holdTimer) { - clearTimeout(this._holdTimer); - this._holdTimer = undefined; - } - document.dispatchEvent( - new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldEnd", - { - bubbles: true, - detail: { - fingers: this.prevPoints.size, - targetTouches: nts.ntt, - touches: nts.nt, - changedTouches: nts.nct, - touchEvent: e - } - }) - ); - for (let i = 0; i < e.changedTouches.length; i++) { - const pt = e.changedTouches.item(i); - if (pt) { - if (this.prevPoints.has(pt.identifier)) { - this.prevPoints.delete(pt.identifier); - } - } - } - - document.removeEventListener("touchmove", this.onReactHoldTouchMove); - document.removeEventListener("touchend", this.onReactHoldTouchEnd); - - e.stopPropagation(); - } - - onReactTouchMove = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); this._holdTimer && clearTimeout(this._holdTimer); @@ -306,6 +249,8 @@ export default class GestureOverlay extends Touchable { } }) ); + + // cleanup any lingering pointers for (let i = 0; i < e.changedTouches.length; i++) { const pt = e.changedTouches.item(i); if (pt) { @@ -324,6 +269,10 @@ export default class GestureOverlay extends Touchable { handleHandDown = async (e: React.TouchEvent) => { this._holdTimer && clearTimeout(this._holdTimer); + + // this chunk of code helps us keep track of which touch events are associated with a hand event + // so that if a hand is held down, but a second hand is interacting with dash, the second hand's events + // won't interfere with the first hand's events. const fingers = new Array<React.Touch>(); for (let i = 0; i < e.touches.length; i++) { const pt: any = e.touches.item(i); @@ -338,6 +287,8 @@ export default class GestureOverlay extends Touchable { } } } + + // this chunk of code determines whether this is a left hand or a right hand, as well as which pointer is the thumb and pointer const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); const rightMost = Math.max(...fingers.map(f => f.clientX)); const leftMost = Math.min(...fingers.map(f => f.clientX)); @@ -354,6 +305,7 @@ export default class GestureOverlay extends Touchable { console.log("not hand"); } this.pointerIdentifier = pointer?.identifier; + runInAction(() => { this._pointerY = pointer?.clientY; if (thumb.identifier === this.thumbIdentifier) { @@ -370,6 +322,7 @@ export default class GestureOverlay extends Touchable { const minX = Math.min(...others.map(f => f.clientX)); const minY = Math.min(...others.map(f => f.clientY)); + // load up the palette collection around the thumb const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc); if (thumbDoc) { runInAction(() => { @@ -393,6 +346,7 @@ export default class GestureOverlay extends Touchable { @action handleHandMove = (e: TouchEvent) => { + // update pointer trackers const fingers = new Array<React.Touch>(); for (let i = 0; i < e.touches.length; i++) { const pt: any = e.touches.item(i); @@ -411,15 +365,19 @@ export default class GestureOverlay extends Touchable { } } } + // update hand trackers const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); if (thumb?.identifier && thumb?.identifier === this.thumbIdentifier) { this._hands.set(thumb.identifier, fingers); } + // loop through every changed pointer for (let i = 0; i < e.changedTouches.length; i++) { const pt = e.changedTouches.item(i); + // if the thumb was moved if (pt && pt.identifier === this.thumbIdentifier && this._thumbY) { if (this._thumbX && this._thumbY) { + // moving a thumb horiz. changes the palette collection selection, moving vert. changes the selection of any menus on the current palette item const yOverX = Math.abs(pt.clientX - this._thumbX) < Math.abs(pt.clientY - this._thumbY); if ((yOverX && this._inkToTextDoc) || this._selectedIndex > -1) { if (Math.abs(pt.clientY - this._thumbY) > (10 * window.devicePixelRatio)) { @@ -433,19 +391,8 @@ export default class GestureOverlay extends Touchable { } } } - - // if (this._thumbX && this._thumbDoc) { - // if (Math.abs(pt.clientX - this._thumbX) > 30) { - // this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); - // this._thumbX = pt.clientX; - // } - // } - // if (this._thumbY && this._inkToTextDoc) { - // if (Math.abs(pt.clientY - this._thumbY) > 20) { - // this._selectedIndex = Math.min(Math.max(0, -Math.ceil((pt.clientY - this._thumbY) / 20)), this._possibilities.length - 1); - // } - // } } + // if the pointer finger was moved if (pt && pt.identifier === this.pointerIdentifier) { this._pointerY = pt.clientY; } @@ -454,27 +401,31 @@ export default class GestureOverlay extends Touchable { @action handleHandUp = (e: TouchEvent) => { + // sometimes, users may lift up their thumb or index finger if they can't stretch far enough to scroll an entire menu, + // so we don't want to just remove the palette when that happens if (e.touches.length < 3) { - // this.onTouchEnd(e); if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier); this._palette = undefined; this.thumbIdentifier = undefined; this._thumbDoc = undefined; + // this chunk of code is for handling the ink to text toolglass let scriptWorked = false; if (NumCast(this._inkToTextDoc?.selectedIndex) > -1) { + // if there is a text option selected, activate it const selectedButton = this._possibilities[this._selectedIndex]; if (selectedButton) { selectedButton.props.onClick(); scriptWorked = true; } } - + // if there isn't a text option selected, dry the ink strokes into ink documents if (!scriptWorked) { this._strokes.forEach(s => { this.dispatchGesture(GestureUtils.Gestures.Stroke, s); }); } + this._strokes = []; this._points = []; this._possibilities = []; @@ -482,6 +433,72 @@ export default class GestureOverlay extends Touchable { } } + /** + * Code for radial menu + */ + onReactHoldTouchMove = (e: TouchEvent) => { + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + document.addEventListener("touchmove", this.onReactHoldTouchMove); + document.addEventListener("touchend", this.onReactHoldTouchEnd); + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldMove", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + } + + /** + * Code for radial menu + */ + onReactHoldTouchEnd = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldEnd", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + for (let i = 0; i < e.changedTouches.length; i++) { + const pt = e.changedTouches.item(i); + if (pt) { + if (this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); + } + } + } + + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + + e.stopPropagation(); + } + @action onPointerDown = (e: React.PointerEvent) => { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { @@ -524,22 +541,28 @@ export default class GestureOverlay extends Touchable { handleLineGesture = (): boolean => { let actionPerformed = false; const B = this.svgBounds; + + // get the two targets at the ends of the line const ep1 = this._points[0]; const ep2 = this._points[this._points.length - 1]; - const target1 = document.elementFromPoint(ep1.X, ep1.Y); const target2 = document.elementFromPoint(ep2.X, ep2.Y); + + // callback function to be called by each target const callback = (doc: Doc) => { if (!this._d1) { this._d1 = doc; } + // we don't want to create a link of both endpoints are the same document (doing so makes drawing an l very hard) else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) { + // we don't want to create a link between ink strokes (doing so makes drawing a t very hard) if (this._d1.type !== "ink" && doc.type !== "ink") { DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link"); actionPerformed = true; } } }; + const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", { bubbles: true, @@ -575,6 +598,7 @@ export default class GestureOverlay extends Touchable { 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 a toolglass is selected and the stroke starts within the toolglass boundaries if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { switch (this.Tool) { case ToolglassTools.InkToText: @@ -583,20 +607,19 @@ export default class GestureOverlay extends Touchable { this._strokes.push(new Array(...this._points)); this._points = []; CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => { - console.log(results); const wordResults = results.filter((r: any) => r.category === "line"); const possibilities: string[] = []; for (const wR of wordResults) { - console.log(wR); if (wR?.recognizedText) { possibilities.push(wR?.recognizedText); } possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString)); } - console.log(possibilities); const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right)); const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left)); const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top)); + + // if we receive any word results from cognitive services, display them runInAction(() => { this._possibilities = possibilities.map(p => <TouchScrollableMenuItem text={p} onClick={() => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: l, Y: t }], p)} />); @@ -609,6 +632,7 @@ export default class GestureOverlay extends Touchable { break; } } + // if we're not drawing in a toolglass try to recognize as gesture else { const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; @@ -638,6 +662,7 @@ export default class GestureOverlay extends Touchable { } } + // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document if (!actionPerformed) { this.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; @@ -762,9 +787,6 @@ export default class GestureOverlay extends Touchable { }}> </div> <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} /> - {/* <div className="pointerBubbles"> - {this._pointers.map(p => <div className="bubble" style={{ translate: `transform(${p.clientX}px, ${p.clientY}px)` }}></div>)} - </div> */} </div>); } } diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 08310786b..10d023d83 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -64,20 +64,15 @@ export abstract class Touchable<T = {}> extends React.Component<T> { case 1: this.handle1PointerDown(te, me); te.persist(); + // -- code for radial menu -- // if (this.holdTimer) { // clearTimeout(this.holdTimer) // this.holdTimer = undefined; // } - // console.log(this.holdTimer); - // console.log(this.holdTimer); break; case 2: this.handle2PointersDown(te, me); - // e.stopPropagation(); break; - // case 5: - // this.handleHandDown(te); - // break; } } } diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index f14c573c3..b8a82ab4d 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -43,12 +43,4 @@ export namespace GestureUtils { } export const GestureRecognizer = new NDollarRecognizer(false); - - export function GestureOptions(name: string, gestureData?: any): (params: {}) => any { - switch (name) { - case Gestures.Box: - break; - } - throw new Error("This means that you're trying to do something with the gesture that hasn't been defined yet. Define it in GestureUtils.ts"); - } }
\ No newline at end of file diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index bb92f62e1..e5740d105 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -161,6 +161,9 @@ const AngleSimilarityThreshold = Deg2Rad(30.0); export class NDollarRecognizer { public Multistrokes: Multistroke[]; + /** + * @IMPORTANT - IF YOU'RE ADDING A NEW GESTURE, BE SURE TO INCREMENT THE NumMultiStrokes CONST RIGHT ABOVE THIS CLASS. + */ constructor(useBoundedRotationInvariance: boolean) // constructor { // |