// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import * as THREE from "three";
import { SceneExtension } from "../SceneExtension";
/** A renderOrder value that should result in rendering after most/all other objects in the scene */
const LATE_RENDER_ORDER = 9999999;
/**
 * A material that interprets the input mesh coordinates in pixel space, regardless of the camera
 * perspective/zoom level.
 */
class FixedSizeMeshMaterial extends THREE.ShaderMaterial {
    constructor({ color, ...params }) {
        super({
            ...params,
            vertexShader: /* glsl */ `
        #include <common>
        uniform vec2 canvasSize;
        void main() {
          vec4 mvPosition = modelViewMatrix * vec4(0., 0., 0., 1.);

          // Adapted from THREE.ShaderLib.sprite
          vec2 scale;
          scale.x = length(vec3(modelMatrix[0].xyz));
          scale.y = length(vec3(modelMatrix[1].xyz));

          gl_Position = projectionMatrix * mvPosition;

          // Add position after projection to maintain constant pixel size
          gl_Position.xy += position.xy / canvasSize * scale * gl_Position.w;
        }
      `,
            fragmentShader: /* glsl */ `
        uniform vec3 color;
        void main() {
          gl_FragColor = vec4(color, 1.0);
        }
      `,
            uniforms: {
                canvasSize: { value: [0, 0] },
                color: { value: new THREE.Color(color).convertSRGBToLinear() },
            },
        });
    }
}
export class MeasurementTool extends SceneExtension {
    static extensionId = "foxglove.MeasurementTool";
    #circleGeometry = new THREE.CircleGeometry(5, 16);
    #circleMaterial = new FixedSizeMeshMaterial({
        color: 0xff0000,
        depthTest: false,
        depthWrite: false,
    });
    #circle1 = new THREE.Mesh(this.#circleGeometry, this.#circleMaterial);
    #circle2 = new THREE.Mesh(this.#circleGeometry, this.#circleMaterial);
    #linePositionAttribute = new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, 0], 3);
    #line = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xff0000 }));
    /**
     * A dashed copy of the line drawn with inverse depth test so the line can still be visible when
     * it's occluded
     */
    #lineOccluded = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineDashedMaterial({
        color: 0xff0000,
        dashSize: 1,
        gapSize: 1,
        depthWrite: false,
        depthFunc: THREE.GreaterDepth, // opposite of default THREE.LessEqualDepth
    }));
    #label;
    #point1NeedsUpdate = false;
    #point2NeedsUpdate = false;
    #point1;
    #point2;
    state = "idle";
    constructor(renderer, name = MeasurementTool.extensionId) {
        super(name, renderer);
        this.#line.userData.picking = false;
        this.#lineOccluded.userData.picking = false;
        this.#circle1.userData.picking = false;
        this.#circle2.userData.picking = false;
        this.#label = renderer.labelPool.acquire();
        this.#label.visible = false;
        this.#label.setBillboard(true);
        this.#label.setSizeAttenuation(false);
        this.#label.setLineHeight(12);
        this.#label.setColor(1, 0, 0);
        // Make the label appear on top of other objects in the scene so it doesn't get clipped/occluded
        this.#label.renderOrder = LATE_RENDER_ORDER;
        this.#label.material.depthTest = false;
        this.#label.material.depthWrite = false;
        this.#label.material.transparent = true;
        this.#lineOccluded.renderOrder = LATE_RENDER_ORDER;
        this.#circle1.renderOrder = LATE_RENDER_ORDER;
        this.#circle2.renderOrder = LATE_RENDER_ORDER;
        this.#line.frustumCulled = false;
        this.#lineOccluded.frustumCulled = false;
        this.#line.geometry.setAttribute("position", this.#linePositionAttribute);
        this.#lineOccluded.geometry.setAttribute("position", this.#linePositionAttribute);
        this.#circle1.visible = false;
        this.#circle2.visible = false;
        this.add(this.#circle1);
        this.add(this.#circle2);
        this.add(this.#line);
        this.add(this.#lineOccluded);
        this.add(this.#label);
        this.#setState("idle");
    }
    dispose() {
        super.dispose();
        this.renderer.labelPool.release(this.#label);
        this.#circleGeometry.dispose();
        this.#circleMaterial.dispose();
        this.#line.geometry.dispose();
        this.#line.material.dispose();
        this.#lineOccluded.geometry.dispose();
        this.#lineOccluded.material.dispose();
        this.renderer.input.removeListener("click", this.#handleClick);
        this.renderer.input.removeListener("mousemove", this.#handleMouseMove);
    }
    startMeasuring() {
        this.#setState("place-first-point");
    }
    stopMeasuring() {
        this.#point1 = this.#point2 = undefined;
        this.#setState("idle");
    }
    startFrame(currentTime, renderFrameId, fixedFrameId) {
        super.startFrame(currentTime, renderFrameId, fixedFrameId);
        this.#circleMaterial.uniforms.canvasSize.value[0] = this.renderer.input.canvasSize.x;
        this.#circleMaterial.uniforms.canvasSize.value[1] = this.renderer.input.canvasSize.y;
    }
    #setState(state) {
        this.state = state;
        switch (state) {
            case "idle":
                this.renderer.input.removeListener("click", this.#handleClick);
                this.renderer.input.removeListener("mousemove", this.#handleMouseMove);
                this.dispatchEvent({ type: "foxglove.measure-end" });
                break;
            case "place-first-point":
                this.#point1 = this.#point2 = undefined;
                this.renderer.input.addListener("click", this.#handleClick);
                this.renderer.input.addListener("mousemove", this.#handleMouseMove);
                this.dispatchEvent({ type: "foxglove.measure-start" });
                break;
            case "place-second-point":
                break;
        }
        this.#updateDistance();
        this.#render();
    }
    #handleMouseMove = (_cursorCoords, worldSpaceCursorCoords, _event) => {
        if (!worldSpaceCursorCoords) {
            return;
        }
        switch (this.state) {
            case "idle":
                break;
            case "place-first-point":
                (this.#point1 ??= new THREE.Vector3()).copy(worldSpaceCursorCoords);
                this.#point1NeedsUpdate = true;
                break;
            case "place-second-point":
                (this.#point2 ??= new THREE.Vector3()).copy(worldSpaceCursorCoords);
                this.#point2NeedsUpdate = true;
                this.#updateDistance();
                break;
        }
        this.#render();
    };
    #updateDistance() {
        if (this.#point1 && this.#point2) {
            this.#label.setText(this.#point1.distanceTo(this.#point2).toFixed(2));
        }
    }
    #handleClick = (_cursorCoords, worldSpaceCursorCoords, _event) => {
        if (!worldSpaceCursorCoords) {
            return;
        }
        switch (this.state) {
            case "idle":
                break;
            case "place-first-point":
                this.#point1 = worldSpaceCursorCoords.clone();
                this.#point1NeedsUpdate = true;
                this.#setState("place-second-point");
                break;
            case "place-second-point":
                this.#point2 = worldSpaceCursorCoords.clone();
                this.#point2NeedsUpdate = true;
                this.#setState("idle");
                break;
        }
        this.#render();
    };
    #render() {
        if (this.#point1) {
            this.#circle1.visible = true;
            this.#circle1.position.copy(this.#point1);
            if (this.#point1NeedsUpdate) {
                this.#linePositionAttribute.setXYZ(0, this.#point1.x, this.#point1.y, this.#point1.z);
                this.#linePositionAttribute.needsUpdate = true;
                this.#lineOccluded.computeLineDistances();
                this.#point1NeedsUpdate = false;
            }
        }
        else {
            this.#circle1.visible = false;
        }
        if (this.#point2) {
            this.#circle2.visible = true;
            this.#circle2.position.copy(this.#point2);
            if (this.#point2NeedsUpdate) {
                this.#linePositionAttribute.setXYZ(1, this.#point2.x, this.#point2.y, this.#point2.z);
                this.#linePositionAttribute.needsUpdate = true;
                this.#lineOccluded.computeLineDistances();
                this.#point2NeedsUpdate = false;
            }
        }
        else {
            this.#circle2.visible = false;
        }
        if (this.#point1 && this.#point2) {
            this.#line.visible = true;
            this.#lineOccluded.visible = true;
            this.#label.visible = true;
            this.#label.position.copy(this.#point1).lerp(this.#point2, 0.5);
        }
        else {
            this.#line.visible = false;
            this.#lineOccluded.visible = false;
            this.#label.visible = false;
        }
        this.renderer.queueAnimationFrame();
    }
}
//# sourceMappingURL=MeasurementTool.js.map