Parse

File Parse dragging.js

This tree is parsed live from the source file.

Classes

  • {{ item.name }}

    • {{ key }}

Not Classes

{{ getTree() }}

Comments

{{ getTreeComments() }}

Source

            /*
---
title: Dragging
dependencies:
    distances.js
files:
    protractor.js
    text/beta.js
---

Dragging tool performs distance tests for all applied points.

    const drag = new Dragging
    drag.initDragging(this)
    drag.onDragMove = this.onDragMove.bind(this)
    drag.onDragEnd = this.onDragEnd.bind(this)

Install points into the map:

    drag.addPoints(this.center, this.point0, this.point1)

Upon a mouse action we can access the discovered points.

    let p = drag.getPoint();
    if(p) {
        p.pen.circle(ctx)
    }

*/


Polypoint.head.mixin('Point', {
    /* Every point gains a _draggable_ property. By default `draggable=true` */
    _draggable: {
        value: true,
        writable: true
    }

    , draggable: {
        get() {
            return this._draggable
        }

        , set(v) {
            this._draggable = false
        }
        // value: {
        // }
    }
})


/*
Polypoint.mixin('Stage', {
    dragging: {
        value: {
            get enabled() {
                return 'foo'
            }

            , set enabled(value) {
                console.log('Active', value)
            }
        }
    }
})*/


class Dragging extends Distances {

    /* click Down<>Up ms delta,*/
    clickSpeed = 300
    clickDragDeadzone = undefined // undefined == 1 radius
    maxWheelValue = 500
    padding = 10

    /* Perform point rotation on on mouse _right_ down.
    False disables this in favour of a contextmenu */
    twistMouse = true

    constructor(stage) {
        super()
        this.stage = stage;
        this.isDragging = false
        this.toy = new Point()
        this.toy2 = new Point({ radius: 10})
    }

    initDragging(stage=this.stage){
        let c = stage.canvas
            , mouse = stage.mouse
            ;

        if(mouse == undefined) {
            console.error('automouse is not imported. Cannot listen to mouse actions.')
            return
        }
        mouse.listen(c, 'mousedown', (c,ev)=> this.onMousedown(stage,c,ev))
        mouse.listen(c, 'mousemove', (c,ev)=> this.onMousemove(stage,c,ev))
        mouse.listen(c, 'mouseup', (c,ev)=> this.onMouseup(stage,c,ev))
        mouse.listen(c, 'wheel', (c,ev)=> this.onWheelInternal(stage,c,ev), {passive: true})
        mouse.listen(c, 'contextmenu', (c,ev)=> this.onContextMenu(stage,c,ev)/*, {passive: true}*/)

        this._near = new Point(mouse.position);
    }

    add(point) {
        return this.addPoints.apply(this, arguments)
    }

    set(many) {
        return this.setPoints.apply(this, arguments)
    }

    onMousedown(stage, canvas, ev) {
        // register point
        // Select near
        return this.primaryActionDown.apply(this, arguments)
    }

    primaryActionDown(stage, canvas, ev){
        /* Capture the down event for the primary mouse button (likely button #0)*/
        this._mousedownDelta = +(new Date)
        this.mousedownOrigin = {x:ev.x, y:ev.y, radius: 20}

        // this.nearOrigin = this.near.copy()
        if(this._near == undefined) {
            // console.log('not near any point at position', this.mousedownOrigin)
            return this.emptyMouseDown(stage, canvas, ev)
        }

        this.mousedownRotationTool(ev)

        let distanceValue = this.distanceValue = this._near.distance2D(this.mousedownOrigin)
        this.downPointDistance = new Point(distanceValue)
        this._mousedown = true
        this._grabbingId = this.stage?.cursor.set('grabbing', this._cursorId)

        this.callPointHandler('onMousedown', ev, this._near)
        // if(this.withinBufferZone(this.downPointDistance)) {
        //     this.onEdgeStartHandler(ev)
        // } else {
        if(this._near.draggable){
            this.onDragStartHandler(ev, this._near)
        }
        // }

    }

    emptyMouseDown(stage, canvas, ev) {
        // console.log('Empty click', stage, ev)
        this.callDoubleHandler('onEmptyDown', ev)
        stage.onEmptyDown && stage.onEmptyDown(ev)
    }

    getPoint(){
        /* return the point previously discovered.*/
        return this._near
    }

    onMousemove(stage, canvas, ev) {
        if(!this._mousedown) {
            // present the possible nearest
            // this._near = this.closest(stage.mouse.position, 100)
            // this._near = this.closest(stage.mouse.position, (v,p)=> v<=p.radius)
            const found = this.intersect(stage.mouse.position, this.padding)
            this._near = found
            this.cursorChange(found)
            this.callPointHandler('onMousemove', ev, found)

            // this.near = this.dis.within(100, this.mouse.position)
            // this._near = this.near(stage.mouse.position, 100)[0]
            return
        }

        let isRightClick = this.isRightClickOkay(ev)
        // Add translation to selected
        // let v = this.nearOrigin.add(ev.x, ev.y)
        if(ev.shiftKey || isRightClick){
            this.onShiftMouseMoveHandler(ev)
            this.didSpin = true
        } else {
            this.onDragMoveHandler(ev)
        }
    }

    isRightClickOkay(ev){
        return (this.twistMouse && (ev.which == 3))
    }

    mousedownRotationTool(ev){
        this.mousedownPoint = Point.from(ev)
        this.mousedownPoint.rotation = this._near.rotation
        this.mousedownPoint.radius = 20
        // this.mousedownOrigin.radians = this._near.radians
        this.mousedownOrigin.rotation = this._near.rotation
    }

    onShiftMouseMoveHandler(ev){
        /* the point should _spin_ rather than move. */
        // ev angle To mouse from origin angle.
        let spinX = this._near.x // this.mousedownOrigin.x
        let spinY = this._near.y // this.mousedownOrigin.y
        let targetCenter = new Point(spinX, spinY);
        let mousePos = stage.mouse.position;
        let downPoint = this.mousedownPoint
        // let rads= calculateAngleDiff(primaryPoint, secondaryPoint)

        let rot = calculateAngle360(targetCenter, mousePos, downPoint.rotation)
        this.toy.update({
                x: spinX
                , y: spinY
                , radius: 20
                // x: this.mousedownPoint.x
                // , y: this.mousedownPoint.y

                /* Add the original origin (the mousedown store of the target)
                rotation, else the result starts from `0` pointing right.
                But we want `0` to originate from the start angle (the big target) */
                , rotation: rot + this.mousedownOrigin.rotation
            })
        this.toy2.update({
                x:spinX
                , y:spinY
                , radius: 20
            }).lookAt(downPoint)
        /* We want to calculate the difference between the angle to the mousedown,
        to the current mouse, with the _origin_ as the center of the big point. */
        let nrot = calculateAngleDiff(this.toy, this.toy2)
        this.toy.text && (this.toy.text.value = nrot.toFixed(0))
        this._near.rotation = downPoint.rotation + nrot
        // this.mousedownOrigin.radians
    }

    cursorChange(found) {
        if(found) {

            /* perform cursor magic */
            if(this._stackedCursor === true) {
                // nothing to do; mouse is stacked.
            } else {
                this._emitCursorHover()
            }

            return
        }

        if(this._stackedCursor === true) {
            this._emitCursorRelease()
        }/* else {
            // nothing to do; mouse is released.
        }*/

    }

    _emitCursorHover() {
        this._stackedCursor = true
        // console.log('enable hover')
        this._cursorId = this.stage?.cursor.set('grab')
    }

    _emitCursorRelease() {
        this._stackedCursor = false
        let id = this._cursorId
        // console.log('disable hover', id)
        this.stage?.cursor.unset(id)
    }

    onMouseup(stage, canvas, ev) {
        this._mousedown = false;
        let nowDelta = +(new Date)
        let delta = nowDelta - this._mousedownDelta
        let isClick = delta <= this.clickSpeed
        let minDistance = this.clickDragDeadzone
        if(minDistance == undefined) {
            minDistance = 20
        }

        let dis = this.dragDistance(ev)
        let withinClickDelta = dis < minDistance
        // console.log('Click Speed =', delta, 'distance', dis, 'is click:', isClick)

        if(this._grabbingId != undefined) {
            this.stage.cursor.unset(this._grabbingId)
            this._grabbingId = undefined
        }

        this.callPointHandler('onMouseup', ev)

        if(isClick && (withinClickDelta)) {
            // If the distance was greater than the max,
            // it's actually a drag
            return this.onClickHander(stage, canvas, ev)
        }

        if((!withinClickDelta) || this.isDragging ) {
            // console.log('This was a drag', dis)
            return this.onDragEndHandler(ev)
        }


        this.onLongClick(stage, canvas, ev, delta)
    }

    onWheelInternal(stage, canvas, ev) {
        let n = this._near;
        if(!n) { return this.onWheelEmpty(ev)};

        let size = event.wheelDelta
        let positive = size > 0
        let compute = Math.abs(size * .01)
        let radius = n.radius;
        let rad = positive? radius*compute: radius/compute;
        n.radius = clamp(rad, 1, this.maxWheelValue)
        n.onResize && n.onResize(ev, stage, canvas)
        // this.onWheel(ev, n)
        this.callDoubleHandler('onWheel', ev, n)
    }

    onLongClick(stage, canvas, ev, delta) {
        console.log('Long Click (not dragged)', delta)
        this.callPointHandler('onLongClick', ev, this._near, delta)
        stage.onLongClick && stage.onLongClick(ev, delta)

    }

    getDownTimeTaken() {
        return +(new Date) - this._mousedownDelta
    }

    onContextMenu(stage,c,ev) {
        // mouse.listen(c, 'contextmenu', (c,ev)=> this.onContextMenu(stage,c,ev), {passive: true})
        let timeDown = this.getDownTimeTaken()

        if(this.twistMouse) {
            // console.log('cancel contextmenu? Time taken:', timeDown)
            let distance = this.mousedownPoint?.distanceTo(ev)
            if((distance && (distance > 10)) && timeDown > 100) {
                ev.preventDefault()
                return // show context
            }

            if(timeDown < 300) {
                return // allow short right click
            }
            // or the distance is far

            ev.preventDefault()
        }
    }

    /* Given the EV with a {x,y}, return the distance from the origin mousedown. */
    dragDistance(ev) {
        if(!this.mousedownOrigin) {
            console.log('! no mouse origin')
            return 0
        }
        let v = distance(this.mousedownOrigin, ev)
        return v
    }

    onClickHander(stage, canvas, ev) {
        // console.log('That was a click not a drag...')
        //
        if(this.isEdgeDragging == true) {
            this.onEdgeEnd(ev)
        }
        let p = this._near
        this.callDoubleHandler('onClick', ev, p)
    }

    callDoubleHandler(name, ev, p, x) {
        /* Given a function name shared by this instance, and the point,
        call the function with the event and arguments.

            callDoubleHandler('onClick', ev, point, ...)

        The function name must exist on this instance, optional on the point.
        */
        let args = [ev, p]
        if(x != undefined) {
            args = Array.from(arguments).slice(1)
        }

        this[name].apply(this, args)
        this.callPointHandler.apply(this, arguments)
    }

    callPointHandler(name, ev, p=this._near, x) {
        /* Call the method `name` on the point, with the event, _if_ the
        point contains the method.

            this.callPointHandler('onMousedown', ev, this._near)

        Apply any arguments after the _point_ to the event

            this.callPointHandler('onLongClick', ev, this._near, delta, { apples: 'green'})
        */
        let args = [ev]
        if(x != undefined) {
            args = args.concat(Array.from(arguments).slice(3))
        }
        p && p[name] && p[name].apply(p, args)
    }

    drawAll(ctx) {
        this.drawIris(ctx)
        this.drawTwists(ctx)
    }

    drawTwists(ctx) {
        this.toy2.pen.indicator(ctx, {color: 'black'})
        this.toy.pen.indicator(ctx, {color: 'red'})
        this.toy.text.label(ctx)
        this.mousedownPoint?.pen.indicator(ctx, {color: 'blue'})
    }

    drawIris(ctx) {
        /* The dynamic highlighter. */
        let p = this.getPoint();
        if(p) {
            p.pen.circle(ctx)
        }
    }

    onWheelEmpty(ev) {}
    onEmptyDown(ev) {}
    onClick(ev) {}
    onDragStart(ev, point){}
    onDragMove(ev) {}
    onDragEnd(ev, point){}
    onWheel(ev, point){}

    onEdgeStart(ev) {
        console.log('onEdgeStart', this._near)
    }

    onEdgeMove(ev) {
        // console.log('onEdgeMove')
    }

    onEdgeEnd(ev) {
        console.log('onEdgeEnd')
    }

    onDragMoveHandler(ev) {
        if(this.isDragging) {
            this.applyXY(ev.x, ev.y)
            this.onDragMove(ev)
            this.callDoubleHandler('onDragMove', ev)
        }

        if(this.isEdgeDragging == true) {
            this.onEdgeMove(ev)
        }
    }

    onDragStartHandler(ev, p){
        this.isDragging = true
        // this.onDragStart(ev, p)
        this.callDoubleHandler('onDragStart', ev, p)
    }

    onEdgeStartHandler(ev) {
        this.isEdgeDragging = true
        this.onEdgeStart(ev)
    }


    withinBufferZone(distancePoint, buffer=this.padding) {
        // distance
        let v = this.distanceValue.distance
        return v > this._near.radius
    }

    onDragEndHandler(ev){
        if(this.isDragging) {
            this.isDragging = false
            // this.onDragEnd(ev)
            this.callDoubleHandler('onDragEnd', ev, this._near)
        }

        if(this.isEdgeDragging == true) {
            this.isEdgeDragging = false
            this.onEdgeEnd(ev)
        }
    }

    applyXY(x,y){
        let offsetSelected = this.downPointDistance.add(x, y)
        this._near.set(offsetSelected.x, offsetSelected.y)
    }
}


class CursorStack {
    /*stash and pop cursor states.
        id = cursor.set(name)
        cursor.unset(id)
    */

    constructor(stage) {
        // this.current = 'default'
        this.icon = 'default'
        this.map = new Map;
        this.stage = stage
    }

    set(name, parallelUnset) {
        // console.log('set cursor')
        let uuid = Math.random().toString(32)
        // if(parallelUnset) {
        //     this.unset(parallelUnset, true)
        // }
        // this.map.set(uuid, [name, this.icon])
        // this.current = uuid
        this.icon = name
        // console.log('set', this.icon)
        this.setMouseIcon(this.icon)
        return uuid
    }

    setMouseIcon(icon){
        this.stage.canvas.style.cursor = icon
    }

    unset(uuid, perform=true) {
        // console.log('unset cursor', uuid)
        // let [name, now] = this.map.get(uuid)
        // this.current = now
        this.icon = 'default'

        // console.log('revert to', this.icon)
        // if(perform) {
            this.setMouseIcon(this.icon)
        // }
    }

}


Polypoint.head.lazierProp('Stage', function cursor(){
    return new CursorStack(this);

    // let dr = this._cursor
    // if(dr == undefined) {
    //     console.log('Returning new lazyProp "CursorStack"')
    //     dr = this._cursorStack = new CursorStack
    //     // dr.initDragging(this)
    // }
    // return dr
});


/* The Stage.dragging utility automatically creates and initates Dragging
when required.

    stage = new Stage;
    stage.dragging.add(new Point(100, 100))
*/
Polypoint.head.lazierProp('Stage', function dragging(){
    console.log('Returning new lazyProp "Dragging"')
    let dr = new Dragging(this)
    dr.initDragging();

    return dr
});


;Polypoint.head.install(Dragging);

copy