From 3bb4d8324b6101a82122ecb31f025c2e0420df89 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Fri, 17 Jan 2020 16:22:38 -0500 Subject: moving around the createPolyline function --- src/client/util/InteractionUtils.tsx | 220 +++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/client/util/InteractionUtils.tsx (limited to 'src/client/util/InteractionUtils.tsx') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx new file mode 100644 index 000000000..1fe95474c --- /dev/null +++ b/src/client/util/InteractionUtils.tsx @@ -0,0 +1,220 @@ +export namespace InteractionUtils { + export const MOUSETYPE = "mouse"; + export const TOUCHTYPE = "touch"; + export const PENTYPE = "pen"; + export const ERASERTYPE = "eraser"; + + const POINTER_PEN_BUTTON = -1; + const REACT_POINTER_PEN_BUTTON = 0; + const ERASER_BUTTON = 5; + + 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 ( + + ); + } + + export class MultiTouchEvent { + 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[], + readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent + ) { } + } + + export interface MultiTouchEventDisposer { (): void; } + + export function MakeMultiTouchTarget( + element: HTMLElement, + startFunc: (e: Event, me: MultiTouchEvent) => void, + ): MultiTouchEventDisposer { + const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent>).detail); + // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent>).detail) : undefined; + // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent>).detail) : undefined; + element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler); + // if (onMultiTouchMoveHandler) { + // element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler); + // } + // if (onMultiTouchEndHandler) { + // element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler); + // } + return () => { + element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler); + // if (onMultiTouchMoveHandler) { + // element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler); + // } + // if (onMultiTouchEndHandler) { + // element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler); + // } + }; + } + + export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent, prevPoints: Map, ignorePen: boolean): React.Touch[] { + const myTouches = new Array(); + for (const pt of mte.touches) { + if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) { + for (const tPt of mte.targetTouches) { + if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) { + if (pt && prevPoints.has(pt.identifier)) { + myTouches.push(pt); + } + } + } + } + } + return myTouches; + } + + 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 + case PENTYPE: + return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); + case ERASERTYPE: + return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); + default: + return e.pointerType === type; + } + } + + 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)); + } + + /** + * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point) + * @param pts - n-arbitrary long list of points + */ + export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } { + const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length; + const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length; + return { X: centerX, Y: centerY }; + } + + /** + * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in + * @param pt1 - new point that corresponds to oldPoint1 + * @param pt2 - new point that corresponds to oldPoint2 + * @param oldPoint1 - previous point 1 + * @param oldPoint2 - previous point 2 + */ + export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { + const threshold = 4; + const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2); + const newDist = TwoPointEuclidist(pt1, pt2); + + /** if they have the same sign, then we are either pinching in or out. + * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) + * so that it can still pan without freaking out + */ + if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) { + return Math.sign(oldDist - newDist); + } + return 0; + } + + /** + * Returns -1 if pinning and pinching out, 0 if not pinning, and 1 if pinching in + * @param pt1 - new point that corresponds to oldPoint1 + * @param pt2 - new point that corresponds to oldPoint2 + * @param oldPoint1 - previous point 1 + * @param oldPoint2 - previous point 2 + */ + export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { + const threshold = 4; + + const pt1Dist = TwoPointEuclidist(oldPoint1, pt1); + const pt2Dist = TwoPointEuclidist(oldPoint2, pt2); + + const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2); + + if (pinching !== 0) { + if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) { + return pinching; + } + } + return 0; + } + + export function IsDragging(oldTouches: Map, newTouches: React.Touch[], leniency: number): boolean { + for (const touch of newTouches) { + if (touch) { + const oldTouch = oldTouches.get(touch.identifier); + if (oldTouch) { + if (TwoPointEuclidist(touch, oldTouch) >= leniency) { + return true; + } + } + } + } + return false; + } + + // These might not be very useful anymore, but I'll leave them here for now -syip2 + { + + + /** + * Returns the type of Touch Interaction from a list of points. + * Also returns any data that is associated with a Touch Interaction + * @param pts - List of points + */ + // export function InterpretPointers(pts: React.Touch[]): { type: Opt, data?: any } { + // const leniency = 200; + // switch (pts.length) { + // case 1: + // return { type: OneFinger }; + // case 2: + // return { type: TwoSeperateFingers }; + // case 3: + // let pt1 = pts[0]; + // let pt2 = pts[1]; + // let pt3 = pts[2]; + // if (pt1 && pt2 && pt3) { + // let dist12 = TwoPointEuclidist(pt1, pt2); + // let dist23 = TwoPointEuclidist(pt2, pt3); + // let dist13 = TwoPointEuclidist(pt1, pt3); + // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`); + // let dist12close = dist12 < leniency; + // let dist23close = dist23 < leniency; + // let dist13close = dist13 < leniency; + // let xor2313 = dist23close ? !dist13close : dist13close; + // let xor = dist12close ? !xor2313 : xor2313; + // // three input xor because javascript doesn't have logical xor's + // if (xor) { + // let points: number[] = []; + // let min = Math.min(dist12, dist23, dist13); + // switch (min) { + // case dist12: + // points = [0, 1, 2]; + // break; + // case dist23: + // points = [1, 2, 0]; + // break; + // case dist13: + // points = [0, 2, 1]; + // break; + // } + // return { type: TwoToOneFingers, data: points }; + // } + // else { + // return { type: ThreeSeperateFingers, data: null }; + // } + // } + // default: + // return { type: undefined }; + // } + // } + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From b04bb12a5942d97eef369936e30b36db62b51f30 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Mon, 27 Jan 2020 19:08:05 -0500 Subject: pen updates --- src/client/util/InteractionUtils.tsx | 5 ++- src/client/views/GestureOverlay.tsx | 52 +++++++++++++++++++--- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 28 ++++++------ src/pen-gestures/ndollar.ts | 7 ++- 5 files changed, 72 insertions(+), 22 deletions(-) (limited to 'src/client/util/InteractionUtils.tsx') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 1fe95474c..ad1d1b5de 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -63,7 +63,7 @@ export namespace InteractionUtils { export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent, prevPoints: Map, ignorePen: boolean): React.Touch[] { const myTouches = new Array(); for (const pt of mte.touches) { - if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) { + if (!ignorePen || ((pt as any).radiusX > 1 && (pt as any).radiusY > 1)) { for (const tPt of mte.targetTouches) { if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) { if (pt && prevPoints.has(pt.identifier)) { @@ -73,6 +73,9 @@ export namespace InteractionUtils { } } } + if (mte.touches.length !== myTouches.length) { + throw Error("opo") + } return myTouches; } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index e8d685139..9dd87554f 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -20,6 +20,12 @@ import { DocumentView } from "./nodes/DocumentView"; import { Transform } from "../util/Transform"; import { DocumentContentsView } from "./nodes/DocumentContentsView"; +/** + * This class handles all of the gesture and touch events first. Native touch and pen + * events should be ignored by all classes and handled up here, and this class will interpret + * these events before dispatching our custom Dash gesture and touch events. Classes that want + * to use touch and pen events should handle custom Dash events, as opposed to native events. + */ @observer export default class GestureOverlay extends Touchable { static Instance: GestureOverlay; @@ -54,6 +60,15 @@ export default class GestureOverlay extends Touchable { GestureOverlay.Instance = this; } + /** + * @description + * Given a touch event, returns three arrays that represent the event's targetTouches, + * changedTouches, and touches after filtering out the touch events that are being handled + * as hands. This helps us separate hand events and touch events, as they are different + * events in the mental model that we are pursuing. + * @param e - Touch event to filter + * @returns \{ newTargetTouches, newChangedTouches, newTouches } + */ getNewTouches(e: React.TouchEvent | TouchEvent) { const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches); const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches); @@ -61,6 +76,7 @@ export default class GestureOverlay extends Touchable { this._hands.forEach((hand) => { for (let i = 0; i < e.targetTouches.length; i++) { const pt = e.targetTouches.item(i); + // if there is a finger in this hand that matches the current point, ignore the current point if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) { ntt.splice(ntt.indexOf(pt), 1); } @@ -83,7 +99,11 @@ export default class GestureOverlay extends Touchable { return { ntt, nct, nt }; } + /** + * @description Handler for the native React touchStart event. + */ onReactTouchStart = (te: React.TouchEvent) => { + // clean up any ghost points that are remaining but don't actually exist const actualPts: React.Touch[] = []; for (let i = 0; i < te.touches.length; i++) { const pt: any = te.touches.item(i); @@ -91,9 +111,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); } } @@ -106,10 +123,11 @@ export default class GestureOverlay extends Touchable { }); ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); - const nts = this.getNewTouches(te); - console.log(nts.nt.length); + // decide whether we should be handling this as a hand event or a touch event + const nts = this.getNewTouches(te); if (nts.nt.length < 5) { + // dispatch a touch event const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); target?.dispatchEvent( new CustomEvent>("dashOnTouchStart", @@ -131,12 +149,17 @@ export default class GestureOverlay extends Touchable { document.addEventListener("touchend", this.onReactTouchEnd); } else { + // handle this event as a hand event this.handleHandDown(te); document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); } } + /** + * @description Handler for the native React touchMove event. Filters and dispatches + * the custom Dash touchMove event. + */ onReactTouchMove = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); document.dispatchEvent( @@ -154,7 +177,11 @@ export default class GestureOverlay extends Touchable { ); } + /** + * @description Handler for the native React touchEnd event. + */ onReactTouchEnd = (e: TouchEvent) => { + // filter and dispatch custom touchEnd event const nts: any = this.getNewTouches(e); document.dispatchEvent( new CustomEvent>("dashOnTouchEnd", @@ -169,6 +196,8 @@ export default class GestureOverlay extends Touchable { } }) ); + + // clean up any points that have ended for (let i = 0; i < e.changedTouches.length; i++) { const pt = e.changedTouches.item(i); if (pt) { @@ -178,6 +207,7 @@ export default class GestureOverlay extends Touchable { } } + // clean up events if (this.prevPoints.size === 0) { document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); @@ -185,8 +215,12 @@ export default class GestureOverlay extends Touchable { e.stopPropagation(); } + /** + * @description Handler for "handDown" events + */ handleHandDown = async (e: React.TouchEvent) => { const fingers = new Array(); + // log all the fingers on the hand for (let i = 0; i < e.touches.length; i++) { const pt: any = e.touches.item(i); if (pt.radiusX > 1 && pt.radiusY > 1) { @@ -200,6 +234,8 @@ export default class GestureOverlay extends Touchable { } } } + + // figure out left/right hand and thumb/pointer finger 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)); @@ -229,6 +265,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)); + // pull up the palette const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc); if (thumbDoc) { runInAction(() => { @@ -246,6 +283,9 @@ export default class GestureOverlay extends Touchable { document.addEventListener("touchend", this.handleHandUp); } + /** + * @description Handler for "handMove" event + */ @action handleHandMove = (e: TouchEvent) => { const fingers = new Array(); @@ -370,7 +410,7 @@ export default class GestureOverlay extends Touchable { else { const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; - if (result && result.Score > 0.7) { + if (result && result.Score > 0.8) { switch (result.Name) { case GestureUtils.Gestures.Box: const target = document.elementFromPoint(this._points[0].X, this._points[0].Y); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 132bf9c8e..a0c75c1b7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -347,7 +347,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); - e.stopPropagation(); + // e.stopPropagation(); } else { e.preventDefault(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e0913b154..82ee1bd63 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -316,21 +316,23 @@ export class DocumentView extends DocComponent(Docu handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (this.Document.onPointerDown) return; const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - this._downX = touch.clientX; - this._downY = touch.clientY; - if (!e.nativeEvent.cancelBubble) { - this._hitTemplateDrag = false; - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; + if (touch) { + this._downX = touch.clientX; + this._downY = touch.clientY; + if (!e.nativeEvent.cancelBubble) { + this._hitTemplateDrag = false; + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); } - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - e.stopPropagation(); } } diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index ef5ca38c6..9e15ada2d 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -168,7 +168,12 @@ export class NDollarRecognizer { // this.Multistrokes = new Array(NumMultistrokes); this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array( - new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146)) + new Array( + new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200), + new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230), + new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160), + new Point(106, 146), //new Point(80, 150), new Point(50, 146), + new Point(30, 143)) )); this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array( new Array(new Point(12, 347), new Point(119, 347)) -- cgit v1.2.3-70-g09d2 From 17dcf7e4d8c2f038dc3a6168b1ff6b8d3cb15536 Mon Sep 17 00:00:00 2001 From: Stanley Yip Date: Thu, 30 Jan 2020 18:39:20 -0500 Subject: i think some bugs are fixed --- src/client/util/InteractionUtils.tsx | 6 +++--- .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src/client/util/InteractionUtils.tsx') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index ad1d1b5de..7194feb2e 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -73,9 +73,9 @@ export namespace InteractionUtils { } } } - if (mte.touches.length !== myTouches.length) { - throw Error("opo") - } + // if (mte.touches.length !== myTouches.length) { + // throw Error("opo") + // } return myTouches; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a0c75c1b7..132bf9c8e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -347,7 +347,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); - // e.stopPropagation(); + e.stopPropagation(); } else { e.preventDefault(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 82ee1bd63..ad28c9b1c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -316,6 +316,7 @@ export class DocumentView extends DocComponent(Docu handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (this.Document.onPointerDown) return; const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + console.log("down"); if (touch) { this._downX = touch.clientX; this._downY = touch.clientY; -- cgit v1.2.3-70-g09d2