Reference Source

src/plugins/DistanceMeasurementsPlugin/DistanceMeasurement.js

import {Dot3D, Label3D, Wire3D} from "../lib/ui/index.js";
import {math} from "../../viewer/scene/math/math.js";
import {Component} from "../../viewer/scene/Component.js";

const tmpVec3a = math.vec3();
const tmpVec3b = math.vec3();
const tmpVec3c = math.vec3();
const tmpVec4a = math.vec4();
const tmpVec4b = math.vec4();
const tmpVec4c = math.vec4();

/**
 * @desc Measures the distance between two 3D points.
 *
 * See {@link DistanceMeasurementsPlugin} for more info.
 */
class DistanceMeasurement extends Component {

    /**
     * @private
     */
    constructor(plugin, cfg = {}) {

        const scene = plugin.viewer.scene;

        super(scene, cfg);

        /**
         * The {@link DistanceMeasurementsPlugin} that owns this DistanceMeasurement.
         * @type {DistanceMeasurementsPlugin}
         */
        this.plugin = plugin;

        const container = cfg.container;
        if (!container) {
            throw "config missing: container";
        }

        this._color = cfg.color || plugin.defaultColor;

        const channel = function(v, defaultIfUndefined) {
            const listeners = [ ];
            let value = v !== undefined ? Boolean(v) : defaultIfUndefined;
            return {
                reg: (l) => listeners.push(l),
                get: () => value,
                set: (v) => {
                    value = v !== undefined ? Boolean(v) : defaultIfUndefined;
                    listeners.forEach(l => l(value));
                }
            };
        };

        this._visible               = channel(cfg.visible,               plugin.defaultVisible);
        this._originVisible         = channel(cfg.originVisible,         plugin.defaultOriginVisible);
        this._targetVisible         = channel(cfg.targetVisible,         plugin.defaultTargetVisible);
        this._axisVisible           = channel(cfg.axisVisible,           plugin.defaultAxisVisible);
        this._xAxisVisible          = channel(cfg.xAxisVisible,          plugin.defaultAxisVisible);
        this._yAxisVisible          = channel(cfg.yAxisVisible,          plugin.defaultAxisVisible);
        this._zAxisVisible          = channel(cfg.zAxisVisible,          plugin.defaultAxisVisible);
        this._axisEnabled           = channel(true,                      plugin.defaultAxisVisible);
        this._wireVisible           = channel(cfg.wireVisible,           plugin.defaultWireVisible);
        this._xLabelEnabled         = channel(cfg.xLabelEnabled,         plugin.defaultXLabelEnabled);
        this._yLabelEnabled         = channel(cfg.yLabelEnabled,         plugin.defaultYLabelEnabled);
        this._zLabelEnabled         = channel(cfg.zLabelEnabled,         plugin.defaultZLabelEnabled);
        this._lengthLabelEnabled    = channel(cfg.lengthLabelEnabled,    plugin.defaultLengthLabelEnabled);
        this._labelsVisible         = channel(cfg.labelsVisible,         plugin.defaultLabelsVisible);
        this._clickable             = channel(false,                     false);
        this._labelsOnWires         = channel(cfg.labelsOnWires,         plugin.defaultLabelsOnWires);
        this._useRotationAdjustment = channel(cfg.useRotationAdjustment, plugin.useRotationAdjustment);

        this._axesBasis = math.identityMat4();
        this.approximate = cfg.approximate;


        const canvas = scene.canvas.canvas;

        const onMouseOver = cfg.onMouseOver ? (event) => {
            cfg.onMouseOver(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseover', event));
        } : null;

        const onMouseLeave = cfg.onMouseLeave ? (event) => {
            cfg.onMouseLeave(event, this);
            canvas.dispatchEvent(new MouseEvent('mouseleave', event));
        } : null;

        const onContextMenu = cfg.onContextMenu ? (event) => {
            cfg.onContextMenu(event, this);
        } : null;

        const onMouseDown  = (event) => canvas.dispatchEvent(new MouseEvent('mousedown', event));
        const onMouseUp    = (event) => canvas.dispatchEvent(new MouseEvent('mouseup', event));
        const onMouseMove  = (event) => canvas.dispatchEvent(new MouseEvent('mousemove', event));
        const onMouseWheel = (event) => canvas.dispatchEvent(new WheelEvent('wheel', event));


        this._cleanups = [ ];

        [ "units", "scale" ].forEach(evt => {
            const handler = scene.metrics.on("units", () => this._update());
            this._cleanups.push(() => scene.metrics.off(handler));
        });

        this._drawables = [ ];

        const registerDrawable = (drawable, visibilityChannels) => {
            const updateVisibility = () => drawable.setVisible(visibilityChannels.every(ch => ch.get()));
            visibilityChannels.forEach(ch => ch.reg(updateVisibility));
            this._drawables.push(drawable);
            this._cleanups.push(() => drawable.destroy());
        };

        const makeWire = (color, thickness, visibilityChannels) => {
            const wire = new Wire3D(scene, container, {
                color: color,
                thickness: thickness,
                thicknessClickable: 6,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 1 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(wire, visibilityChannels);
            return {
                setEnds: (p0, p1) => wire.setEnds(p0, p1),
                setColor: value => wire.setColor(value)
            };
        };
        this._lengthWire = makeWire(this._color, 2, [ this._visible, this._wireVisible ]);
        this._xAxisWire  = makeWire("red",       1, [ this._visible, this._axisEnabled, this._axisVisible, this._xAxisVisible ]);
        this._yAxisWire  = makeWire("green",     1, [ this._visible, this._axisEnabled, this._axisVisible, this._yAxisVisible ]);
        this._zAxisWire  = makeWire("blue",      1, [ this._visible, this._axisEnabled, this._axisVisible, this._zAxisVisible ]);

        const makeLabel = (color, zIndexOffset, visibilityChannels) => {
            const label = new Label3D(scene, container, {
                fillColor: color,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + zIndexOffset : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            registerDrawable(label, visibilityChannels);
            return {
                setFillColor:  value => label.setFillColor(value),
                setPosOnWire:  (p0, p1, offset, labelMinAxisLength) => label.setPosOnWire(p0, p1, offset, labelMinAxisLength),
                setPosBetween: (p0, p1, p2) => label.setPosBetween(p0, p1, p2),
                setText:       str => label.setText(str.replace(/ /g, " "))
            };
        };
        this._lengthLabel = makeLabel(this._color, 4, [ this._visible, this._wireVisible, this._labelsVisible, this._clickable, this._axisEnabled, this._lengthLabelEnabled ]);
        this._xAxisLabel  = makeLabel("red",       3, [ this._visible, this._axisEnabled, this._axisVisible, this._xAxisVisible, this._labelsVisible, this._clickable, this._xLabelEnabled ]);
        this._yAxisLabel  = makeLabel("green",     3, [ this._visible, this._axisEnabled, this._axisVisible, this._yAxisVisible, this._labelsVisible, this._clickable, this._yLabelEnabled ]);
        this._zAxisLabel  = makeLabel("blue",      3, [ this._visible, this._axisEnabled, this._axisVisible, this._zAxisVisible, this._labelsVisible, this._clickable, this._zLabelEnabled ]);

        const makeDot = (cfg, visibilityChannels) => {
            const dot = new Dot3D(scene, cfg, container, {
                fillColor: this._color,
                zIndex: plugin.zIndex !== undefined ? plugin.zIndex + 2 : undefined,
                onMouseOver,
                onMouseLeave,
                onMouseWheel,
                onMouseDown,
                onMouseUp,
                onMouseMove,
                onContextMenu
            });
            dot.on("worldPos", () => this._update());
            registerDrawable(dot, visibilityChannels);
            return dot;
        };
        this._originDot = makeDot(cfg.origin, [ this._visible, this._originVisible ]);
        this._targetDot = makeDot(cfg.target, [ this._visible, this._targetVisible ]);

        this._update();
    }

    _update() {
        if (! this._targetDot) {
            return;
        }

        const p0 = this._originDot.worldPos;
        const p1 = this._targetDot.worldPos;
        const axesBasis = this._axesBasis;
        const delta = math.subVec3(p1, p0, tmpVec3a);
        const factors = math.transformVec3(axesBasis, delta, delta);

        const measurementOrientationVertical = this._useRotationAdjustment.get() && Math.abs(delta[1]) > 0;

        const setWireCoordinates = (xEnd, zStart) => {

            const metrics = this.plugin.viewer.scene.metrics;
            const scale = metrics.scale;
            const unit = metrics.unitsInfo[metrics.units].abbrev;

            const setAxisLabelCoords = (label, a, b, offsetIdx) => {
                if (this._labelsOnWires.get()) {
                    label.setPosOnWire(a, b, 0, this.plugin.labelMinAxisLength);
                } else {
                    label.setPosOnWire(p0, p1, offsetIdx * 35, 0);
                }
            };
            const unitStr = len => (this._approximate ? " ~ " : " = ") + len.toFixed(2) + unit;

            this._xAxisWire.setEnds(p0, xEnd);
            setAxisLabelCoords(this._xAxisLabel, p0, xEnd, 1);
            this._xAxisLabel.setText("X" + unitStr(math.distVec3(p0, xEnd) * scale));

            this._yAxisWire.setEnds(xEnd, zStart);
            setAxisLabelCoords(this._yAxisLabel, xEnd, zStart, 2);
            this._yAxisLabel.setText("Y" + unitStr(math.distVec3(xEnd, zStart) * scale));

            this._zAxisWire.setEnds(zStart, p1);
            setAxisLabelCoords(this._zAxisLabel, zStart, p1, 3);
            this._zAxisLabel.setText((measurementOrientationVertical ? "" : "Z") + unitStr(math.distVec3(zStart, p1) * scale));

            this._lengthWire.setEnds(p0, p1);
            setAxisLabelCoords(this._lengthLabel, p0, p1, 0);
            this._length = math.distVec3(p0, p1) * scale;
            this._lengthLabel.setText(unitStr(this._length));
        };

        if (measurementOrientationVertical) {
            tmpVec3c[0] = p0[0];
            tmpVec3c[1] = p1[1];
            tmpVec3c[2] = p0[2];

            setWireCoordinates(p0, tmpVec3c);
        }
        else {
            tmpVec3b[0] = p0[0] + axesBasis[0] * factors[0];
            tmpVec3b[1] = p0[1] + axesBasis[4] * factors[0];
            tmpVec3b[2] = p0[2] + axesBasis[8] * factors[0];

            tmpVec3c[0] = tmpVec3b[0] + axesBasis[1] * factors[1];
            tmpVec3c[1] = tmpVec3b[1] + axesBasis[5] * factors[1];
            tmpVec3c[2] = tmpVec3b[2] + axesBasis[9] * factors[1];

            setWireCoordinates(tmpVec3b, tmpVec3c);
        }
    }

    /**
     * Sets the axes basis for the measurement.
     *
     * The value is a 4x4 matrix where each column-vector defines an axis and must have unit length.
     *
     * This is the ```identity``` matrix by default, meaning the measurement axes are the same as the world axes.
     *
     * @param {number[]} value
     */
    set axesBasis(value) {
        this._axesBasis.set(value);
        this._update();
    }

    /**
     * Gets the axes basis for the measurement.
     *
     * The value is a 4x4 matrix where each column-vector defines an axis and must have unit length.
     *
     * This is the ```identity``` matrix by default, meaning the measurement axes are the same as the world axes.
     *
     * @type {number[]}
     */
    get axesBasis() {
        return this._axesBasis;
    }

    /**
     * Sets whether this DistanceMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    set approximate(approximate) {
        approximate = approximate !== false;
        if (this._approximate === approximate) {
            return;
        }
        this._approximate = approximate;
        this._update();
    }

    /**
     * Gets whether this DistanceMeasurement indicates that its measurement is approximate.
     *
     * This is ````true```` by default.
     *
     * @type {Boolean}
     */
    get approximate() {
        return this._approximate;
    }

    /**
     * Gets the origin {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get origin() {
        return this._originDot;
    }

    /**
     * Gets the target {@link Dot3D}.
     *
     * @type {Dot3D}
     */
    get target() {
        return this._targetDot;
    }

    /**
     * Gets the World-space direct point-to-point distance between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target}.
     *
     * @type {Number}
     */
    get length() {
        return this._length;
    }

    get color() {
        return this._color;
    }

    set color(value) {
        this._color = value;
        this._originDot.setFillColor(value);
        this._targetDot.setFillColor(value);
        this._lengthWire.setColor(value);
        this._lengthLabel.setFillColor(value);
    }

    /**
     * Sets whether this DistanceMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    set visible(value) {
        this._visible.set(value);
    }

    /**
     * Gets whether this DistanceMeasurement is visible or not.
     *
     * @type {Boolean}
     */
    get visible() {
        return this._visible.get();
    }

    /**
     * Sets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set originVisible(value) {
        this._originVisible.set(value);
    }

    /**
     * Gets if the origin {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get originVisible() {
        return this._originVisible.get();
    }

    /**
     * Sets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    set targetVisible(value) {
        this._targetVisible.set(value);
    }

    /**
     * Gets if the target {@link Dot3D} is visible.
     *
     * @type {Boolean}
     */
    get targetVisible() {
        return this._targetVisible.get();
    }

    /**
     * Sets if the measurement is adjusted based on rotation
     *
     * @type {Boolean}
     */
    set useRotationAdjustment(value) {
        this._useRotationAdjustment.set(value);
        this._update();
    }

    /**
     * Gets if the measurement is adjusted based on rotation
     *
     * @type {Boolean}
     */
    get useRotationAdjustment() {
        return this._useRotationAdjustment.get();
    }

    /**
     * Sets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are enabled.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set axisEnabled(value) {
        this._axisEnabled.set(value);
    }

    /**
     * Gets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are enabled.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get axisEnabled() {
        return this._axisEnabled.get();
    }

    /**
     * Sets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set axisVisible(value) {
        this._axisVisible.set(value);
    }

    /**
     * Gets if the axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get axisVisible() {
        return this._axisVisible.get();
    }

    /**
     * Sets if the X-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set xAxisVisible(value) {
        this._xAxisVisible.set(value);
    }

    /**
     * Gets if the X-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get xAxisVisible() {
        return this._xAxisVisible.get();
    }

    /**
     * Sets if the Y-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set yAxisVisible(value) {
        this._yAxisVisible.set(value);
    }

    /**
     * Gets if the Y-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get yAxisVisible() {
        return this._yAxisVisible.get();
    }

    /**
     * Sets if the Z-axis-aligned wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    set zAxisVisible(value) {
        this._zAxisVisible.set(value);
    }

    /**
     * Gets if the Z-axis-aligned wires between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} are visible.
     *
     * Wires are only shown if enabled and visible.
     *
     * @type {Boolean}
     */
    get zAxisVisible() {
        return this._zAxisVisible.get();
    }

    /**
     * Sets if the direct point-to-point wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * @type {Boolean}
     */
    set wireVisible(value) {
        this._wireVisible.set(value);
    }

    /**
     * Gets if the direct point-to-point wire between {@link DistanceMeasurement#origin} and {@link DistanceMeasurement#target} is visible.
     *
     * @type {Boolean}
     */
    get wireVisible() {
        return this._wireVisible.get();
    }

    /**
     * Sets if the labels are visible except the length label.
     *
     * @type {Boolean}
     */
    set labelsVisible(value) {
        this._labelsVisible.set(value);
    }

    /**
     * Gets if the labels are visible.
     *
     * @type {Boolean}
     */
    get labelsVisible() {
        return this._labelsVisible.get();
    }

    /**
     * Sets if the x label is enabled.
     *
     * @type {Boolean}
     */
    set xLabelEnabled(value) {
        this._xLabelEnabled.set(value);
    }

    /**
     * Gets if the x label is enabled.
     *
     * @type {Boolean}
     */
    get xLabelEnabled(){
        return this._xLabelEnabled.get();
    }

    /**
     * Sets if the y label is enabled.
     *
     * @type {Boolean}
     */
    set yLabelEnabled(value) {
        this._yLabelEnabled.set(value);
    }

    /**
     * Gets if the y label is enabled.
     *
     * @type {Boolean}
     */
    get yLabelEnabled(){
        return this._yLabelEnabled.get();
    }

    /**
     * Sets if the z label is enabled.
     *
     * @type {Boolean}
     */
    set zLabelEnabled(value) {
        this._zLabelEnabled.set(value);
    }

    /**
     * Gets if the z label is enabled.
     *
     * @type {Boolean}
     */
    get zLabelEnabled(){
        return this._zLabelEnabled.get();
    }

    /**
     * Sets if the length label is enabled.
     *
     * @type {Boolean}
     */
    set lengthLabelEnabled(value) {
        this._lengthLabelEnabled.set(value);
    }

    /**
     * Gets if the length label is enabled.
     *
     * @type {Boolean}
     */
    get lengthLabelEnabled(){
        return this._lengthLabelEnabled.get();
    }

    /**
     * Sets if labels should be positioned on the wires.
     *
     * @type {Boolean}
     */
    set labelsOnWires(value) {
        this._labelsOnWires.set(value);
        this._update();
    }

    /**
     * Gets if labels should be positioned on the wires.
     *
     * @type {Boolean}
     */
    get labelsOnWires() {
        return this._labelsOnWires.get();
    }

    /**
     * Sets if this DistanceMeasurement appears highlighted.
     * @param highlighted
     */
    setHighlighted(highlighted) {
        this._drawables.forEach(d => d.setHighlighted(highlighted));
    }

    /**
     * Sets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events, or ignore mouse events altogether.
     *
     * @type {Boolean}
     */
    set clickable(value) {
        this._clickable.set(!!value);
        this._drawables.forEach(d => d.setClickable(this._clickable.get()));
    }

    /**
     * Gets if the wires, dots ad labels will fire "mouseOver" "mouseLeave" and "contextMenu" events.
     *
     * @type {Boolean}
     */
    get clickable() {
        return this._clickable.get();
    }

    /**
     * @private
     */
    destroy() {
        this._cleanups.forEach(cleanup => cleanup());
        super.destroy();
    }
}

export {DistanceMeasurement};