Source: widget/help/view2/LaserBeam.js

(function() {
    /**
     * @typedef {Object} Help4.widget.help.view2.LaserBeam.Params
     * @property {Help4.widget.help.Widget} widget
     * @property {Help4.control2.container.Container} contentView
     * @property {Help4.control2.container.Container} tileView
     * @property {Help4.widget.help.view2.Tile.Descriptor} descriptor
     */

    /**
     * Laser beam control to connect tile and hotspot.
     * @augments Help4.jscore.ControlBase
     * @property {Help4.widget.help.Widget} __widget
     * @property {Help4.control2.container.Container} __contentView
     * @property {Help4.control2.container.Container} __tileView
     * @property {Help4.widget.help.view2.Tile.Descriptor} descriptor
     * @property {?Help4.control2.hotspot.Connected} _hotspotControl
     */
    Help4.widget.help.view2.LaserBeam = class extends Help4.jscore.ControlBase {
        /**
         * @constructor
         * @param {Help4.widget.help.view2.LaserBeam.Params} params
         */
        constructor(params) {
            const {TYPES: T} = Help4.jscore.ControlBase;
            super(params, {
                params: {
                    widget:      {type: T.instance, mandatory: true, readonly: true, private: true},
                    contentView: {type: T.instance, mandatory: true, readonly: true, private: true},
                    tileView:    {type: T.instance, mandatory: true, readonly: true, private: true},
                    descriptor:  {type: T.string, mandatory: true}
                },
                statics: {
                    _hotspotControl: {destroy: false}
                }
            });

            _show.call(this);
        }

        /**
         * @param {Help4.widget.help.view2.View} view
         * @param {?Help4.widget.help.view2.Tile.Descriptor} [descriptor = null]
         */
        static show(view, descriptor = null) {
            const {__widget, _laserBeam, _contentView, _tileView} = view;

            const {showLaserBeam} = __widget.getContext().configuration.help;
            if (!showLaserBeam) return;

            if (!descriptor && _laserBeam) descriptor = _laserBeam.descriptor;

            if (descriptor) {
                _laserBeam?.destroy();  // destroy possible previous beam

                const {LaserBeam} = Help4.widget.help.view2;
                view._laserBeam = /** @type {Help4.widget.help.view2.LaserBeam} */ new LaserBeam({
                    widget: __widget,
                    contentView: _contentView,
                    tileView: _tileView,
                    descriptor
                });
            }
        }

        /**
         * @param {Help4.widget.help.view2.View} view
         * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
         */
        static hide(view, descriptor) {
            const {_laserBeam} = view;
            if (!_laserBeam) return;

            // only remove laser beam if correct tile descriptor
            if (descriptor === _laserBeam.descriptor) {
                _laserBeam.destroy();
                view._laserBeam = null;
            }
        }

        /** @override */
        destroy() {
            this._hotspotControl?.cleanConnections();
            super.destroy();
        }
    }

    /**
     * @memberof Help4.widget.help.view2.LaserBeam#
     * @private
     */
    function _show() {
        const {__widget, __contentView, __tileView, descriptor} = this;

        const {
            /** @type {Help4.control2.bubble.Panel} */ panel,
            /** @type {Help4.typedef.SystemConfiguration} */ configuration
        } = /** @type {Help4.widget.help.Widget.Context} */ __widget.getContext();


        const tileControl = /** @type {?Help4.control2.Tile} */ __tileView.get({byMetadata: {descriptor, type: 'tile'}});
        const hotspotControl = /** @type {?Help4.control2.hotspot.Connected} */ __contentView.get({byMetadata: {descriptor, type: 'hotspot'}});

        if (tileControl && hotspotControl) {
            const area = /** @type {Help4.control2.AreaXYWH} */ panel.getArea();
            const pcp = /** @type {Help4.control2.ConnectionPoints} */ panel.getConnectionPoints();
            const {
                /** @type {Help4.control2.PositionXY} */ m
            } = /** @type {Help4.control2.ConnectionPoints} */ hotspotControl.getConnectionPoints();

            /** @type {?Help4.control2.PositionXY} */
            const point = panel.docked
                ? (panel.rtl ? pcp.r : pcp.l)
                : _getPoint(m, area, pcp);

            if (point) {
                const connectionParams = {
                    ...Help4.HOTSPOT_CONNECTION_DEFAULTS,
                    svg: configuration.core.isRemoteMode,
                    point
                };

                hotspotControl.addConnection(connectionParams);
                this._hotspotControl = hotspotControl;
            }
        }
    }

    /**
     * @memberof Help4.widget.help.view2.LaserBeam#
     * @private
     * @param {Help4.control2.PositionXY} hotspotPoint
     * @param {Help4.control2.AreaXYWH} panelArea
     * @param {Help4.control2.ConnectionPoints} panelConnectionPoints
     * @returns {?Help4.control2.PositionXY}
     */
    function _getPoint(hotspotPoint, panelArea, panelConnectionPoints) {
        if (Help4.isPointInRect(hotspotPoint, panelArea)) {
            // hotspot mid-point is below panel
            return null;  // XXX: add support
        } else {
            // create all possible lines from panel to hotspot
            const lines = {};
            ['l', 't', 'r', 'b'].forEach(pos => {
                // line from panel to hotspot
                const line = {
                    x1: hotspotPoint.x,
                    y1: hotspotPoint.y,
                    x2: panelConnectionPoints[pos].x,
                    y2: panelConnectionPoints[pos].y
                };

                // only if it does not cross the panel
                if (!Help4.getRectIntersect(panelArea, line, -1)) {
                    lines[pos] = line;
                }
            });

            const keys = Object.keys(lines);
            const nbr = keys.length;

            // just one line does not touch the panel
            if (nbr === 1) return panelConnectionPoints[keys[0]];

            // multiple lines do not touch the panel
            if (nbr > 1) {
                // calculate line distances
                const distances = {};
                keys.forEach(key => {
                    const {x1, y1, x2, y2} = lines[key];
                    distances[key] = Math.pow(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2), 0.5);
                });

                // select the shortest line
                /** @type {number[]} */ const values = Object.values(distances);
                const min = Math.min(...values);
                const idx = values.indexOf(min);
                const key = Object.keys(distances)[idx];
                return panelConnectionPoints[key];
            }

            // every line touches the panel
            return null;
        }
    }
})();