Home Reference Source

cables_dev/cables_ui/src/ui/glpatch/glcable.js

import { Logger } from "cables-shared-client";
import gluiconfig from "./gluiconfig.js";
import text from "../text.js";
import userSettings from "../components/usersettings.js";
import GlPort from "./glport.js";


/**
 * rendering cables for links
 *
 * @export
 * @class GlCable
 */
export default class GlCable
{
    constructor(glPatch, splineDrawer, buttonRect, type, link, subpatch)
    {
        this.LINETYPE_CURVED = 0;
        this.LINETYPE_STRAIGHT = 1;
        this.LINETYPE_SIMPLE = 2;
        this.LINETYPE_HANGING = 3;

        this._log = new Logger("glcable");

        this._x = 0;
        this._y = 0;
        this._x2 = 0;
        this._y2 = 0;

        this._points = [];

        this._buttonSize = gui.theme.patch.cableButtonSize || 17;
        this._linetype = this.LINETYPE_CURVED;

        this._subPatch = subpatch;
        this._glPatch = glPatch;
        this._buttonRect = buttonRect;
        this._type = type;
        this._disposed = false;
        this._visible = true;
        if (link) this._visible = link.visible;

        this._link = link;

        this._splineDrawer = splineDrawer;
        this._splineIdx = this._splineDrawer.getSplineIndex();

        this._buttonRect.setShape(1);
        this._buttonRect.visible = false;

        this._buttonRect.on("hover", () =>
        {
            this.setCloseToMouse(true);
            this._link.cableHoverChanged(this, true);
            this.updateColor();
        });

        this._buttonRect.on("unhover", () =>
        {
            this._unHover();
        });

        gui.on("themeChanged", () =>
        {
            this._oldx = this._oldy = this._oldx2 = this._oldy2 = 0;
            this._buttonSize = gui.theme.patch.cableButtonSize || 17;
            this._setPositionButton();
            this._updateLinePos();
        });

        this._distFromPort = 0;
        this._updateDistFromPort();

        this.updateMouseListener();

        this.updateLineStyle();
        this.updateColor();
    }

    get subPatch() { return this._subPatch; }

    updateLineStyle()
    {
        this._oldx = this._oldy = this._oldx2 = this._oldy2 = 0;
        this._tension = 0.1;
        this._curvedSimple = false;
        const oldLineType = this._linetype;
        this._linetype = this.LINETYPE_CURVED;

        if (userSettings.get("linetype") == "simple") this._linetype = this.LINETYPE_SIMPLE;
        if (userSettings.get("linetype") == "straight") this._linetype = this.LINETYPE_STRAIGHT;
        if (userSettings.get("linetype") == "h1")
        {
            this._linetype = this.LINETYPE_HANGING;
            this._tension = 0.0;
        }
        if (userSettings.get("linetype") == "h2")
        {
            this._linetype = this.LINETYPE_HANGING;
            this._tension = 0.2;
        }
        if (userSettings.get("linetype") == "h3")
        {
            this._linetype = this.LINETYPE_HANGING;
            this._tension = 0.3;
        }

        this._updateDistFromPort();
        this._updateLinePos();
    }

    updateMouseListener()
    {
        if (this._visible)
        {
            if (!this._listenerMousemove)
                this._listenerMousemove = this._glPatch.on("mousemove", this._checkCollide.bind(this));
        }

        if ((!this._visible || this._disposed) && this._listenerMousemove)
        {
            this._glPatch.off(this._listenerMousemove);
            this._listenerMousemove = null;
        }
    }

    setCloseToMouse(b)
    {
        if (this._buttonRect.interactive != b)
        {
            this._buttonRect.visible =
            this._buttonRect.interactive = b;
            if (!b) this._unHover();
        }
    }

    _unHover()
    {
        this.setCloseToMouse(false);
        this._link.cableHoverChanged(this, false);
        this.updateColor();
    }

    updateVisible()
    {
        const old = this._visible;
        this._visible = (this._subPatch == this._glPatch.getCurrentSubPatch());
        if (this._disposed) this._visible = false;

        if (old != this._visible)
        {
            if (!this._visible) this.setCloseToMouse(false);
            this.updateMouseListener();
        }
    }

    set visible(v)
    {
        // is this even needed ? all cables are drawn because the splinedrawer is bound to a specific subpatch anyway....
        if (this._visible != v) this._oldx = null;
        this._visible = v;
        this._updateLinePos();
    }

    _checkCollide(e)
    {
        if (this._disposed) return;
        if (this._glPatch.isAreaSelecting) return;
        if (!this._visible) return false;
        if (this._subPatch != this._glPatch.getCurrentSubPatch()) return false;

        if (this._glPatch.isDraggingOps())
        {
            if (this._glPatch.getNumSelectedOps() == 1)
            {
                const op = this._glPatch.getOnlySelectedOp();
                const glop = this._glPatch.getGlOp(op);
                if (glop.displayType === glop.DISPLAY_SUBPATCH)
                {
                    // const portsIn = gui.patchView.getSubPatchExposedPorts(op.patchId.get(), CABLES.PORT_DIR_IN);
                    // const portsOut = gui.patchView.getSubPatchExposedPorts(op.patchId.get(), CABLES.PORT_DIR_OUT);

                    // if (portsIn.length < 1 || portsOut.lengh < 1 || !portsIn[0] || !portsOut[0]) return false;
                    // if (!(portsIn[0].type == portsOut[0].type == this._type)) return false;
                }
                else
                if (op.portsIn.length > 0 && op.portsOut.length > 0)
                {
                    if (!(
                        op.getFirstPortIn().type == this._type &&
                        op.getFirstPortOut().type == this._type))
                        return false;
                }
            }
        }

        this.collideMouse(e, this._x, this._y - this._distFromPort, this._x2, this._y2 + this._distFromPort, this._glPatch.viewBox.mousePatchX, this._glPatch.viewBox.mousePatchY, this._buttonSize * 0.4);
    }

    dispose()
    {
        this._disposed = true;

        this._splineDrawer.setSplineColor(this._splineIdx, [0, 0, 0, 1]);
        this._buttonRect.setColor(0, 0, 0, 1);

        this._splineDrawer.deleteSpline(this._splineIdx);
        this.updateVisible();
        this.updateMouseListener();
    }

    _updateDistFromPort()
    {
        if (this._linetype == this.LINETYPE_SIMPLE || this._curvedSimple)
        {
            this._distFromPort = 0;
            return;
        }
        if (Math.abs(this._y - this._y2) < gluiconfig.portHeight * 2) this._distFromPort = gluiconfig.portHeight * 0.5;
        else this._distFromPort = gluiconfig.portHeight * 2.9; // magic number...?!
    }

    _subdivide(inPoints, divs)
    {
        const arr = [];
        const subd = divs || 4;
        let newLen = (inPoints.length - 4) * (subd - 1);

        if (newLen != arr.length) arr.length = Math.floor(Math.abs(newLen));
        let count = 0;

        function ip(x0, x1, x2, t)// Bezier
        {
            const r = (x0 * (1 - t) * (1 - t) + 2 * x1 * (1 - t) * t + x2 * t * t);
            return r;
        }

        for (let i = 3; i < inPoints.length - 3; i += 3)
        {
            for (let j = 0; j < subd; j++)
            {
                for (let k = 0; k < 3; k++)
                {
                    const p = ip(
                        (inPoints[i + k - 3] + inPoints[i + k]) / 2,
                        inPoints[i + k + 0],
                        (inPoints[i + k + 3] + inPoints[i + k + 0]) / 2,
                        j / subd
                    );
                    arr[count] = p;
                    count++;
                }
            }
        }

        arr.push(inPoints[inPoints.length - 3], inPoints[inPoints.length - 2], inPoints[inPoints.length - 1]);
        arr.unshift(inPoints[0], inPoints[1], inPoints[2]);

        return arr;
    }

    _updateLinePos()
    {
        if (this._disposed) return;
        this._updateDistFromPort();

        // "hanging" cables
        // this._splineDrawer.setSpline(this._splineIdx,
        //     this._subdivide(
        //         [
        //             this._x, this._y, 0,
        //             this._x, this._y, 0,
        //             this._x, this._y - this._distFromPort * 2.0, 0,
        //             this._x2, this._y2 + Math.abs((this._y2 - this._y)) * 1.7, 0,
        //             this._x2, this._y2, 0,
        //             this._x2, this._y2, 0,
        //         ]));


        if (this._oldx != this._x || this._oldy != this._y || this._oldx2 != this._x2 || this._oldy2 != this._y2)
        {
            let posX = this._oldx = this._x;
            let posX2 = this._oldy = this._y;
            this._oldx2 = this._x2;
            this._oldy2 = this._y2;

            if (this._x !== this._x2 !== 0)
            {
                posX = this._x + gluiconfig.portWidth / 2 - 5;
                posX2 = this._x2 + gluiconfig.portWidth / 2 - 5;
            }

            if (this._linetype == this.LINETYPE_CURVED)
            {
                if (this._x == this._x2 || Math.abs(this._x - this._x2) < 50)
                {
                    this._curvedSimple = true;
                    this._updateDistFromPort();

                    this._points = // this._subdivide(
                        [
                            posX, this._y, gluiconfig.zPosCables,
                            // posX, this._y, gluiconfig.zPosCables,
                            posX2, this._y2, gluiconfig.zPosCables,
                            posX2, this._y2, gluiconfig.zPosCables
                        ];
                    // );

                    this._splineDrawer.setSpline(this._splineIdx, this._points);
                }
                else
                {
                    if (this._curvedSimple)
                    {
                        this._curvedSimple = false;
                        this._updateDistFromPort();
                    }

                    const distY = Math.abs(this._y - this._y2);

                    this._points =
                        [
                            posX, this._y, gluiconfig.zPosCables,
                            posX, this._y - (distY * 0.002) - this._distFromPort * (gui.theme.patch.cablesCurveY || 1.25), gluiconfig.zPosCables,

                            (posX + posX2) * 0.5, (this._y + this._y2) * 0.5, gluiconfig.zPosCables, // center point

                            posX2, this._y2 + (distY * 0.002) + this._distFromPort * (gui.theme.patch.cablesCurveY || 1.25), gluiconfig.zPosCables,
                            posX2, this._y2, gluiconfig.zPosCables,
                            posX2, this._y2, gluiconfig.zPosCables

                        ];

                    // console.log(gui.theme.patch.cablesSubDivde);
                    // for (let i = 0; i < (gui.theme.patch.cablesSubDivde); i++)
                    this._points = this._subdivide(this._points, 5);

                    // this._points.unshift(posX, this._y, 0);
                    // this._points.unshift(posX, this._y, 0);
                    // this._points.unshift(posX, this._y, 0);
                    // this._points.unshift(posX, this._y, 0);
                    // this._points.push(posX2, this._y2, 0);
                    // this._points.push(posX2, this._y2, 0);

                    this._splineDrawer.setSpline(this._splineIdx, this._points);
                }
            }
            else if (this._linetype == this.LINETYPE_STRAIGHT)
            {
                // straight lines...

                this._points = [
                    posX, this._y, gluiconfig.zPosCables,
                    posX, this._y - this._distFromPort, gluiconfig.zPosCables,
                    posX2, this._y2 + this._distFromPort, gluiconfig.zPosCables,
                    posX2, this._y2, gluiconfig.zPosCables
                ];

                this._splineDrawer.setSpline(this._splineIdx, this._points);
            }
            if (this._linetype == this.LINETYPE_SIMPLE)
            {
                this._points = [
                    posX, this._y, gluiconfig.zPosCables,
                    posX, this._y, gluiconfig.zPosCables,
                    posX2, this._y2, gluiconfig.zPosCables,
                    posX2, this._y2, gluiconfig.zPosCables
                ];

                this._splineDrawer.setSpline(this._splineIdx, this._points);
            }
        }
        if (this._visible)
        {

            // this._lineDrawer.setLine(this._lineIdx0, this._x, this._y, this._x, this._y - this._distFromPort);
            // this._lineDrawer.setLine(this._lineIdx1, this._x, this._y - this._distFromPort, this._x2, this._y2 + this._distFromPort);
            // this._lineDrawer.setLine(this._lineIdx2, this._x2, this._y2 + this._distFromPort, this._x2, this._y2);
        }
        else
        {
            // this._splineDrawer.hideSpline(this._splineIdx);

            // this._splineDrawer.setSpline(this._splineIdx,
            //     [
            //         0, 0, 0,
            //         0, 0, 0,
            //         0, 0, 0,
            //         0, 0, 0
            //     ]);


            // this._lineDrawer.setLine(this._lineIdx0, 0, 0, 0, 0);
            // this._lineDrawer.setLine(this._lineIdx1, 0, 0, 0, 0);
            // this._lineDrawer.setLine(this._lineIdx2, 0, 0, 0, 0);
        }
    }


    _setPositionButton()
    {
        this._buttonRect.setShape(1);
        this._buttonRect.setSize(this._buttonSize, this._buttonSize);
        this._buttonRect.setPosition(
            this._x + ((this._x2 - this._x) / 2) - this._buttonSize / 2,
            (this._y + this._buttonSize) + (((this._y2 - this._buttonSize) - (this._y + this._buttonSize)) / 2) - this._buttonSize / 2,
            gluiconfig.zPosCableButtonRect);
    }

    setPosition(x, y, x2, y2)
    {
        if (!(this._x != x || this._y != y || this._x2 != x2 || this._y2 != y2)) return;

        this._x = x;
        this._y = y;

        this._x2 = x2;
        this._y2 = y2;

        this._updateLinePos();
        this._setPositionButton();

        this._buttonRect.visible = false;
    }

    get hovering()
    {
        return this._buttonRect._hovering || (this._glOpIn && this._glOpIn.hovering) || (this._glOpOut && this._glOpOut.hovering);
    }

    updateColor()
    {
        if (this._disposed)
        {
            // console.log((new Error()).stack);
            return;
        }

        let hover = this.hovering;
        let selected = false;

        if (this._link)
        {
            if (this._link.isAPortHovering())hover = true;
            if (this._link.isAOpSelected())selected = true;
        }

        const col = GlPort.getColor(this._link.type, false, false);

        this._splineDrawer.setSplineColor(this._splineIdx, col);
        this._splineDrawer.setSplineColorInactive(this._splineIdx, GlPort.getInactiveColor(this._link.type));
        this._splineDrawer.setSplineColorBorder(this._splineIdx, GlPort.getColorBorder(this._link.type, hover, selected));

        this._buttonRect.setColor(col[0], col[1], col[2], col[3]);
    }

    setColor(r, g, b, a)
    {
        this.updateColor();
    }

    isHoveredButtonRect()
    {
        if (this._glPatch.isDraggingPort()) return false;
        return this.collideMouse(null, this._x, this._y - this._distFromPort, this._x2, this._y2 + this._distFromPort, this._glPatch.viewBox.mousePatchX, this._glPatch.viewBox.mousePatchY, 7);
    }

    setSpeed(speed)
    {
        if (this._glPatch.vizFlowMode != 0)
            this._splineDrawer.setSplineSpeed(this._splineIdx, speed);
    }

    collideLine(x1, y1, x2, y2)
    {
        if (!this._visible) return;
        for (let i = 0; i < this._points.length - 3; i += 3)
        {
            const found = this.collideLineLine(x1, y1, x2, y2, this._points[i + 0], this._points[i + 1], this._points[i + 3], this._points[i + 4]);

            if (found) return true;
        }
        return false;
    }

    collideMouse(e, x1, y1, x2, y2, cx, cy, r)
    {
        // if (this._glPatch.isDraggingPort()) return;
        // if (this._glPatch.isDraggingPort()) this._glPatch.showOpCursor(false);
        // canlink ???

        if (this._disposed)
        {
            this._log.warn("disposed already!!!?!");
        }

        const perf = CABLES.UI.uiProfiler.start("glcable collideMouse");

        // is either end INSIDE the circle?
        // if so, return true immediately
        const inside1 = this._collidePointCircle(x1, y1, cx, cy, r);
        const inside2 = this._collidePointCircle(x2, y2, cx, cy, r);
        if (inside1 || inside2)
        {
            perf.finish();
            return true;
        }

        // get length of the line
        let distX = x1 - x2;
        let distY = y1 - y2;
        const len = Math.sqrt((distX * distX) + (distY * distY));

        // get dot product of the line and circle
        const dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / len ** 2;

        // find the closest point on the line
        const closestX = x1 + (dot * (x2 - x1));
        const closestY = y1 + (dot * (y2 - y1));

        // is this point actually on the line segment?
        // if so keep going, but if not, return false
        const onSegment = this._collideLinePoint(x1, y1, x2, y2, closestX, closestY);

        if (!onSegment)
        {
            this.setCloseToMouse(false);

            perf.finish();
            return false;
        }

        // get distance to closest point
        distX = closestX - cx;
        distY = closestY - cy;

        const distance = Math.sqrt((distX * distX) + (distY * distY));
        const mouseOverLineAndOpButNotDragging = this._glPatch.isMouseOverOp() && !this._glPatch.isDraggingOps();


        if (distance <= r && !mouseOverLineAndOpButNotDragging)
        {
            const selectedOp = gui.patchView.getSelectedOps()[0];
            if (selectedOp && (!selectedOp.portsIn || !selectedOp.portsOut || selectedOp.portsIn.length == 0 || selectedOp.portsOut.length == 0)) return;



            if (
                this._glPatch.isDraggingOps() &&
                gui.patchView.getSelectedOps().length == 1 &&

                (
                    (this._link._glOpIn.op.id == selectedOp.id) ||
                    (this._link._glOpOut.op.id == selectedOp.id)
                )
            )
            {
                // no self hovering/linking
                return false;
            }

            this.updateColor();
            this._buttonRect.setPosition(closestX - this._buttonSize / 2, closestY - this._buttonSize / 2, gluiconfig.zPosCableButtonRect);

            this._glPatch._cablesHoverButtonRect = this._buttonRect;

            this.setCloseToMouse(true);

            this.updateColor();

            this._glPatch.setHoverLink(e, this._link);
            this._glPatch._dropInCircleRect = this._buttonRect;
            this._glPatch._dropInCircleLink = this._link;

            if (this._glPatch.cablesHoverText) this._glPatch.cablesHoverText.setPosition(closestX + 10, closestY - 10);

            gui.showInfo(text.linkAddCircle);

            perf.finish();
            return true;
        }
        else
        {
            if (this._buttonRect.visible) this._glPatch.setHoverLink(e, null);

            this.setCloseToMouse(false);

            perf.finish();
            return false;
        }
    }

    setText(t)
    {
        if (this._buttonRect._hovering && this._glPatch.cablesHoverText)
        {
            this._glPatch.cablesHoverText.text = t || "";
        }
    }

    _dist(x, y, x2, y2)
    {
        const distX = x - x2;
        const distY = y - y2;
        return Math.sqrt((distX * distX) + (distY * distY));
    }

    // POINT/CIRCLE
    _collidePointCircle(px, py, cx, cy, r)
    {
        // get distance between the point and circle's center
        // using the Pythagorean Theorem
        // const distX = px - cx;
        // const distY = py - cy;
        // const distance = Math.sqrt((distX * distX) + (distY * distY));
        const distance = this._dist(px, py, cx, cy);

        // if the distance is less than the circle's
        // radius the point is inside!
        if (distance <= r)
        {
            return true;
        }
        return false;
    }


    // LINE/POINT
    _collideLinePoint(x1, y1, x2, y2, px, py)
    {
        // get distance from the point to the two ends of the line
        const d1 = this._dist(px, py, x1, y1);
        const d2 = this._dist(px, py, x2, y2);

        // get the length of the line
        const lineLen = this._dist(x1, y1, x2, y2);

        // since  are so minutely accurate, add
        // a little buffer zone that will give collision
        const buffer = 0.1; // higher # = less accurate

        // if the two distances are equal to the line's
        // length, the point is on the line!
        // note we use the buffer here to give a range,
        // rather than one #
        if (d1 + d2 >= lineLen - buffer && d1 + d2 <= lineLen + buffer)
        {
            return true;
        }
        return false;
    }

    collideLineLine(x1, y1, x2, y2, x3, y3, x4, y4)
    {
        // calculate the distance to intersection point
        let uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
        let uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));

        // if uA and uB are between 0-1, lines are colliding
        if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1)
        {
            // optionally, draw a circle where the lines meet
            let intersectionX = x1 + (uA * (x2 - x1));
            let intersectionY = y1 + (uA * (y2 - y1));
            //   fill(255,0,0);
            //   noStroke();
            //   ellipse(intersectionX,intersectionY, 20,20);

            return true;
        }
        return false;
    }
}