// 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 { t } from "i18next";
import * as _ from "lodash-es";
import * as THREE from "three";
import CameraControls from "camera-controls";
import { CoordinateFrame, makePose } from "@foxglove/studio-base/panels/ThreeDeeRender/transforms";
import { SceneExtension } from "../SceneExtension";
import { DEFAULT_CAMERA_STATE } from "../camera";
import { PRECISION_DEGREES, PRECISION_DISTANCE } from "../settings";
import { on3dSceneReset } from "../RendererOverlay";
const DISPLAY_FRAME_NOT_FOUND = "DISPLAY_FRAME_NOT_FOUND";
const snapshotInRenderFrame = makePose();
const tempVec3 = new THREE.Vector3();
const tempSpherical = new THREE.Spherical();
const FOLLOW_TF_PATH = ["general", "followTf"];
const AZIMUTH_SENSITIVITY = 0.2;
const POLAR_SENSITIVITY = 0.2;
const CAMERA_DISTANCE_OFFSET = 1500;
export class CameraStateSettings extends SceneExtension {
    renderer;
    #unfollowSnapshotFrameIds;
    unfollowPoseSnapshot;
    #controls;
    #isUpdatingCameraState = false;
    #canvas;
    #cameraGroup;
    #perspectiveCamera;
    #orthographicCamera;
    #aspect;
    #defaultConfig = false;
    #updateInterval = null;
    #lastTargetOffset = new THREE.Vector3();
    #isDraggingForRotation = false;
    #lastMouseX = 0;
    #lastMouseY = 0;
    // Keyboard panning state
    #pressedKeys = new Set();
    // Orbiting state
    #orbitActive = false;
    #orbitSpeedDegPerSec = 15; // degrees per second
    #orbitDirection = 1; // 1 or -1
    constructor(renderer, canvas, aspect) {
        super("foxglove.CameraStateSettings", renderer);
        this.renderer = renderer;
        CameraControls.install({ THREE: THREE });
        this.renderer.on("transformTreeUpdated", this.#handleTransformTreeUpdated);
        this.renderer.settings.errors.on("update", this.#handleErrorChange);
        this.renderer.settings.errors.on("clear", this.#handleErrorChange);
        this.renderer.settings.errors.on("remove", this.#handleErrorChange);
        this.#canvas = canvas;
        this.#perspectiveCamera = new THREE.PerspectiveCamera();
        this.#orthographicCamera = new THREE.OrthographicCamera();
        this.#cameraGroup = new THREE.Group();
        this.#cameraGroup.add(this.#perspectiveCamera);
        this.#cameraGroup.add(this.#orthographicCamera);
        this.add(this.#cameraGroup);
        this.#controls = new CameraControls(this.#perspectiveCamera, this.#canvas);
        this.#controls.dampingFactor = 0.05;
        this.#controls.mouseButtons.left = CameraControls.ACTION.NONE;
        this.#controls.mouseButtons.right = CameraControls.ACTION.TRUCK;
        this.#controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY;
        this.#controls.touches.one = CameraControls.ACTION.TOUCH_SCREEN_PAN;
        this.#controls.infinityDolly = true;
        this.#controls.dollySpeed = 0.25;
        this.#perspectiveCamera.rotation.set(0, Math.PI, Math.PI);
        this.#perspectiveCamera.up.set(0, 1, 0);
        this.#controls.update(0);
        const euler = new THREE.Euler().setFromQuaternion(this.#perspectiveCamera.quaternion, "ZYX");
        this.#perspectiveCamera.quaternion.setFromEuler(euler);
        this.#controls.addEventListener("update", this.#handleControlsUpdate);
        on3dSceneReset.on("resetView", () => {
            if (this.#updateInterval) {
                clearInterval(this.#updateInterval);
            }
            window.sessionStorage.removeItem("camera3DState");
            this.#setDefaultCameraState(true);
        });
        const controls = this.#controls;
        // Track time between frames for consistent keyboard pan speed
        let lastTime = performance.now();
        const animate = () => {
            const now = performance.now();
            const dt = Math.max(0, (now - lastTime) / 1000);
            lastTime = now;
            // Apply keyboard panning before updating controls
            this.#applyKeyboardPan(dt);
            // Apply orbiting if enabled
            this.#updateOrbit(dt);
            controls.update(1.0);
            requestAnimationFrame(animate);
        };
        animate();
        canvas.tabIndex = 1000;
        this.#aspect = aspect;
        canvas.addEventListener("mousedown", this.#handleMouseDown);
        canvas.addEventListener("mousemove", this.#handleMouseMove);
        window.addEventListener("mouseup", this.#handleMouseUp);
        canvas.addEventListener("mouseleave", this.#handleMouseLeave);
        canvas.addEventListener("contextmenu", (event) => event.preventDefault());
        // Keyboard listeners (canvas must be focused)
        canvas.addEventListener("keydown", this.#handleKeyDown);
        canvas.addEventListener("keyup", this.#handleKeyUp);
        canvas.addEventListener("blur", this.#handleBlur);
        // CAMERA "ICON"
        this.cameraIcon();
        this.#setDefaultCameraState();
    }
    // Set camera state to default
    #setDefaultCameraState(resetCamera = false) {
        if (resetCamera) {
            this.#lastMouseX = 0;
            this.#lastMouseY = 0;
            this.#lastTargetOffset.set(0, 0, 0);
            this.#isDraggingForRotation = false;
            this.#orthographicCamera.position.set(0, 0, 0);
            this.renderer.updateConfig((draft) => {
                draft.cameraState = _.cloneDeep(DEFAULT_CAMERA_STATE);
            });
        }
        let restoredCameraState = false;
        setTimeout(() => {
            let defaultCameraState = undefined;
            try {
                const cameraStateString = window.sessionStorage.getItem("camera3DState");
                if (cameraStateString) {
                    const cameraState = JSON.parse(cameraStateString);
                    if (cameraState && "distance" in cameraState && "phi" in cameraState) {
                        defaultCameraState = cameraState;
                        restoredCameraState = true;
                    }
                }
            }
            catch (error) {
                console.error(error);
            }
            const activeCamera = this.getActiveCamera();
            activeCamera.updateMatrixWorld(true);
            this.renderer.queueAnimationFrame();
            const cameraState = defaultCameraState ?? DEFAULT_CAMERA_STATE;
            this.#applyCameraStateToControls(cameraState, undefined);
            this.#controls.update(0);
            this.renderer.queueAnimationFrame();
        }, 10);
        // Rotates the camera to the default position
        setTimeout(() => {
            if (restoredCameraState) {
                const currentTarget = new THREE.Vector3();
                this.#controls.getTarget(currentTarget);
                this.#controls.setTarget(currentTarget.x, currentTarget.y, CAMERA_DISTANCE_OFFSET);
                this.#controls.update(2);
                this.#defaultConfig = true;
                return;
            }
            const currentTarget = new THREE.Vector3();
            this.#controls.getTarget(currentTarget);
            const currentPosition = new THREE.Vector3();
            this.#perspectiveCamera.getWorldPosition(currentPosition);
            const offset = currentPosition.clone().sub(currentTarget);
            const spherical = new THREE.Spherical();
            spherical.setFromVector3(offset);
            spherical.theta = Math.max(0.001, Math.min(Math.PI - 0.001, spherical.theta + Math.PI));
            const newOffset = new THREE.Vector3().setFromSpherical(spherical);
            const newCameraPosition = currentTarget.clone().add(newOffset);
            this.#controls.setLookAt(newCameraPosition.x, newCameraPosition.y, newCameraPosition.z, currentTarget.x, currentTarget.y, restoredCameraState ? currentTarget.z : currentTarget.z + CAMERA_DISTANCE_OFFSET, false);
            this.#controls.update(0);
            this.renderer.queueAnimationFrame();
            this.#defaultConfig = true;
        }, 100);
    }
    cameraIcon() {
        const dotRadius = 7.5;
        const rodRadius = 2;
        const rodLength = 60;
        const rodColor = 0x5724E8;
        const dotColor = 0xffffff;
        const numRods = 4;
        const forwardTiltAngle = -Math.PI / 6;
        const dotMaterial = new THREE.MeshBasicMaterial({ color: dotColor });
        const rodMaterial = new THREE.MeshBasicMaterial({ color: rodColor });
        const dotGeometry = new THREE.SphereGeometry(dotRadius, 16, 16);
        const rodGeometry = new THREE.CylinderGeometry(rodRadius, rodRadius, rodLength, 8);
        const compositeObject = new THREE.Group();
        const dotMesh = new THREE.Mesh(dotGeometry, dotMaterial);
        compositeObject.add(dotMesh);
        for (let i = 0; i < numRods; i++) {
            const pivot = new THREE.Object3D();
            const angle = Math.PI / 4 + i * (Math.PI / 2);
            pivot.rotation.y = angle;
            pivot.rotation.z = forwardTiltAngle;
            const rodMesh = new THREE.Mesh(rodGeometry, rodMaterial);
            rodMesh.rotation.z = Math.PI / 2;
            rodMesh.position.x = rodLength / 2;
            pivot.add(rodMesh);
            compositeObject.add(pivot);
        }
        compositeObject.rotation.x = (Math.PI / 2) * 3;
        compositeObject.position.z += -100;
        compositeObject.scale.set(1 / 3, 1 / 3, 1 / 3);
        this.add(compositeObject);
    }
    dispose() {
        this.renderer.off("transformTreeUpdated", this.#handleTransformTreeUpdated);
        this.renderer.settings.errors.off("update", this.#handleErrorChange);
        this.renderer.settings.errors.off("clear", this.#handleErrorChange);
        this.renderer.settings.errors.off("remove", this.#handleErrorChange);
        this.#controls.removeEventListener("update", this.#handleControlsUpdate);
        this.#canvas.removeEventListener("mousedown", this.#handleMouseDown);
        this.#canvas.removeEventListener("mousemove", this.#handleMouseMove);
        window.removeEventListener("mouseup", this.#handleMouseUp);
        this.#canvas.removeEventListener("mouseleave", this.#handleMouseLeave);
        this.#canvas.removeEventListener("contextmenu", (event) => event.preventDefault());
        // Keyboard listeners cleanup
        this.#canvas.removeEventListener("keydown", this.#handleKeyDown);
        this.#canvas.removeEventListener("keyup", this.#handleKeyUp);
        this.#canvas.removeEventListener("blur", this.#handleBlur);
        this.#controls.dispose();
        if (this.#updateInterval) {
            clearInterval(this.#updateInterval);
        }
        super.dispose();
    }
    // Public API to control orbiting externally
    startOrbit(speedDegPerSec, direction) {
        if (speedDegPerSec != undefined)
            this.#orbitSpeedDegPerSec = Math.max(0.1, speedDegPerSec);
        if (direction === 1 || direction === -1)
            this.#orbitDirection = direction;
        this.#orbitActive = true;
    }
    stopOrbit() {
        this.#orbitActive = false;
    }
    settingsNodes() {
        return [this.#cameraSettingsNode(), this.#frameSettingsNode()];
    }
    #cameraSettingsNode() {
        const config = this.renderer.config;
        const camera = config.cameraState;
        const handler = this.handleSettingsAction;
        return {
            path: ["cameraState"],
            node: {
                label: t("threeDee:view"),
                actions: [
                    { type: "action", id: "reset-camera", label: t("threeDee:reset") },
                ],
                handler,
                fields: {
                    syncCamera: {
                        label: t("threeDee:syncCamera"),
                        input: "boolean",
                        error: this.renderer.cameraSyncError(),
                        value: config.scene.syncCamera ?? false,
                        help: t("threeDee:syncCameraHelp"),
                    },
                    distance: {
                        label: t("threeDee:distance"),
                        input: "number",
                        step: 1,
                        precision: PRECISION_DISTANCE,
                        value: camera.distance ?? config.cameraState.distance,
                    },
                    perspective: {
                        label: t("threeDee:perspective"),
                        input: "boolean",
                        value: camera.perspective,
                    },
                    targetOffset: {
                        label: t("threeDee:target"),
                        input: "vec3",
                        labels: ["X", "Y", "Z"],
                        precision: PRECISION_DISTANCE,
                        value: [
                            ...(camera.targetOffset ?? config.cameraState.targetOffset),
                        ],
                    },
                    thetaOffset: {
                        label: t("threeDee:theta"),
                        input: "number",
                        step: 1,
                        precision: PRECISION_DEGREES,
                        value: camera.thetaOffset ?? config.cameraState.thetaOffset,
                    },
                    phi: {
                        label: t("threeDee:phi"),
                        input: "number",
                        step: 1,
                        precision: PRECISION_DEGREES,
                        value: camera.phi ?? config.cameraState.phi,
                    },
                    ...(camera.perspective && {
                        fovy: {
                            label: t("threeDee:fovy"),
                            input: "number",
                            step: 1,
                            precision: PRECISION_DEGREES,
                            value: camera.fovy,
                        },
                    }),
                    near: {
                        label: t("threeDee:near"),
                        input: "number",
                        step: DEFAULT_CAMERA_STATE.near,
                        precision: PRECISION_DISTANCE,
                        value: camera.near,
                    },
                    far: {
                        label: t("threeDee:far"),
                        input: "number",
                        step: 1,
                        precision: PRECISION_DISTANCE,
                        value: camera.far ?? config.cameraState.far,
                    },
                },
            },
        };
    }
    #frameSettingsNode() {
        const config = this.renderer.config;
        const handler = this.handleSettingsAction;
        let followTfOptions = this.renderer.coordinateFrameList;
        const followFrameId = this.renderer.followFrameId;
        this.#updateFollowTfError();
        const followTfValue = config.followTf ?? followFrameId;
        if (followTfValue != undefined &&
            !this.renderer.transformTree.hasFrame(followTfValue)) {
            followTfOptions = [
                {
                    label: CoordinateFrame.DisplayName(followTfValue),
                    value: followTfValue,
                },
                ...followTfOptions,
            ];
        }
        const followTfError = this.renderer.settings.errors.errors.errorAtPath(FOLLOW_TF_PATH);
        const followModeOptions = [
            { label: t("threeDee:pose"), value: "follow-pose" },
            { label: t("threeDee:position"), value: "follow-position" },
            { label: t("threeDee:fixed"), value: "follow-none" },
        ];
        const followModeValue = config.followMode;
        return {
            path: ["general"],
            node: {
                label: t("threeDee:frame"),
                fields: {
                    followTf: {
                        label: t("threeDee:displayFrame"),
                        help: t("threeDee:displayFrameHelp"),
                        input: "select",
                        options: followTfOptions,
                        value: followTfValue,
                        error: followTfError,
                    },
                    followMode: {
                        label: t("threeDee:followMode"),
                        help: t("threeDee:followModeHelp"),
                        input: "select",
                        options: followModeOptions,
                        value: followModeValue,
                    },
                },
                defaultExpansionState: "expanded",
                handler,
            },
        };
    }
    handleSettingsAction = (action) => {
        if (action.action === "perform-node-action" &&
            action.payload.id === "reset-camera") {
            const previousState = _.cloneDeep(this.renderer.config.cameraState);
            this.renderer.updateConfig((draft) => {
                draft.cameraState = _.cloneDeep(DEFAULT_CAMERA_STATE);
            });
            this.#applyCameraStateToControls(this.renderer.config.cameraState, previousState);
            this.renderer.emit("cameraMove", this.renderer);
            return;
        }
        if (action.action !== "update" || action.payload.path.length === 0) {
            return;
        }
        const { path, value } = action.payload;
        const category = path[0];
        if (category === "cameraState") {
            const previousState = _.cloneDeep(this.renderer.config.cameraState);
            if (path[1] === "syncCamera") {
                this.renderer.updateConfig((draft) => {
                    draft.scene.syncCamera = value;
                });
            }
            else {
                this.renderer.updateConfig((draft) => {
                    _.set(draft, path, value);
                });
                this.#applyCameraStateToControls(this.renderer.config.cameraState, previousState);
                this.renderer.emit("cameraMove", this.renderer);
            }
        }
        if (category === "general") {
            const previousState = _.cloneDeep(this.renderer.config.cameraState);
            if (path[1] === "followTf") {
                const followTf = value;
                this.renderer.updateConfig((draft) => {
                    draft.followTf = followTf;
                });
                this.#updateFollowFrameId();
                this.renderer.settings.errors.clearPath(FOLLOW_TF_PATH);
            }
            else if (path[1] === "followMode") {
                const followMode = value;
                this.renderer.updateConfig((draft) => {
                    if (draft.followMode === "follow-none" ||
                        followMode === "follow-pose") {
                        draft.cameraState.targetOffset = [
                            ...DEFAULT_CAMERA_STATE.targetOffset,
                        ];
                    }
                    if (followMode === "follow-pose") {
                        draft.cameraState.thetaOffset = DEFAULT_CAMERA_STATE.thetaOffset;
                    }
                    draft.followMode = followMode;
                });
                this.#applyCameraStateToControls(this.renderer.config.cameraState, previousState);
                this.renderer.emit("cameraMove", this.renderer);
            }
            else {
                this.renderer.updateConfig((draft) => {
                    _.set(draft, path, value);
                });
            }
        }
    };
    startFrame(currentTime, renderFrameId, fixedFrameId) {
        const followMode = this.renderer.config.followMode;
        if (followMode === "follow-pose" ||
            fixedFrameId === CoordinateFrame.FALLBACK_FRAME_ID ||
            renderFrameId === CoordinateFrame.FALLBACK_FRAME_ID) {
            this.#unfollowSnapshotFrameIds = undefined;
            this.unfollowPoseSnapshot = undefined;
            this.#cameraGroup.position.set(0, 0, 0);
            this.#cameraGroup.quaternion.set(0, 0, 0, 1);
            return;
        }
        const poseSnapshot = this.#getUnfollowPoseSnapshot(fixedFrameId, renderFrameId, currentTime);
        const transformTree = this.renderer.transformTree;
        if (poseSnapshot) {
            const appliedTransform = Boolean(transformTree.apply(snapshotInRenderFrame, poseSnapshot, renderFrameId, fixedFrameId, fixedFrameId, currentTime, currentTime));
            if (!appliedTransform) {
                return;
            }
            if (followMode === "follow-position") {
                this.#cameraGroup.position.set(0, 0, 0);
            }
            else {
                this.#cameraGroup.position.set(snapshotInRenderFrame.position.x, snapshotInRenderFrame.position.y, snapshotInRenderFrame.position.z);
            }
            this.#cameraGroup.quaternion.set(snapshotInRenderFrame.orientation.x, snapshotInRenderFrame.orientation.y, snapshotInRenderFrame.orientation.z, snapshotInRenderFrame.orientation.w);
        }
    }
    #handleControlsUpdate = () => {
        if (!this.#isUpdatingCameraState && !this.#isDraggingForRotation) {
            const newDistance = this.#controls.distance;
            this.#controls.getTarget(tempVec3);
            const state = this.renderer.config.cameraState;
            const distChanged = Math.abs(state.distance - newDistance) > 1e-6;
            const targetChanged = !_.isEqual(state.targetOffset, [
                tempVec3.x,
                tempVec3.y,
                tempVec3.z,
            ]);
            const spherical = new THREE.Spherical();
            const cameraPosition = new THREE.Vector3();
            this.#controls.getPosition(cameraPosition);
            spherical.setFromVector3(cameraPosition.clone().sub(tempVec3));
            const phiDeg = THREE.MathUtils.radToDeg(spherical.phi);
            const thetaDeg = -THREE.MathUtils.radToDeg(spherical.theta);
            const phiChanged = Math.abs(state.phi - phiDeg) > 1e-3;
            const thetaChanged = Math.abs(state.thetaOffset - thetaDeg) > 1e-3;
            if (distChanged || targetChanged || phiChanged || thetaChanged) {
                this.renderer.updateConfig((draft) => {
                    if (distChanged)
                        draft.cameraState.distance = newDistance;
                    if (targetChanged)
                        draft.cameraState.targetOffset = [tempVec3.x, tempVec3.y, tempVec3.z];
                    if (phiChanged)
                        draft.cameraState.phi = phiDeg;
                    if (thetaChanged)
                        draft.cameraState.thetaOffset = thetaDeg;
                });
                this.#lastTargetOffset.copy(tempVec3);
                this.renderer.emit("cameraMove", this.renderer);
            }
        }
        if (this.#defaultConfig) {
            this.#updateInterval = setInterval(() => {
                this.#saveCameraState();
            }, 200);
            this.#saveCameraState();
            this.#defaultConfig = false;
        }
    };
    #saveCameraState = () => {
        const config = JSON.stringify(this.renderer.config.cameraState);
        if (config) {
            window.sessionStorage.setItem("camera3DState", config);
        }
        else {
            console.warn("Filed to save camera state");
        }
    };
    #handleMouseDown = (event) => {
        // Ensure canvas has focus for keyboard input
        this.#canvas.focus();
        if (event.button === 0) {
            event.preventDefault();
            this.#isDraggingForRotation = true;
            this.#lastMouseX = event.clientX;
            this.#lastMouseY = event.clientY;
        }
    };
    // Keyboard input handlers and per-frame pan
    #handleKeyDown = (event) => {
        const key = event.key.toLowerCase();
        // Orbit controls
        if (key === "o") {
            // Shift+O toggles and flips direction
            if (event.shiftKey)
                this.#orbitDirection *= -1;
            this.#orbitActive = !this.#orbitActive;
            event.preventDefault();
            return;
        }
        if (key === "q") {
            // slower
            this.#orbitSpeedDegPerSec = Math.max(0.5, this.#orbitSpeedDegPerSec * 0.8);
            event.preventDefault();
            return;
        }
        if (key === "e") {
            // faster
            this.#orbitSpeedDegPerSec = Math.min(360, this.#orbitSpeedDegPerSec * 1.25);
            event.preventDefault();
            return;
        }
        // Panning keys
        if (key === "w" || key === "a" || key === "s" || key === "d" || key === "shift") {
            this.#pressedKeys.add(key);
            event.preventDefault();
        }
    };
    #handleKeyUp = (event) => {
        const key = event.key.toLowerCase();
        if (this.#pressedKeys.has(key)) {
            this.#pressedKeys.delete(key);
            event.preventDefault();
        }
    };
    #handleBlur = () => {
        this.#pressedKeys.clear();
    };
    #applyKeyboardPan(dt) {
        if (this.#pressedKeys.size === 0)
            return;
        // Scale pan speed with camera distance for consistent feel
        const baseSpeed = Math.max(0.001, this.#controls.distance * 0.5); // world units per second
        const speed = this.#pressedKeys.has("shift") ? baseSpeed * 2 : baseSpeed;
        let dx = 0;
        let dy = 0;
        if (this.#pressedKeys.has("a"))
            dx -= speed * dt; // left
        if (this.#pressedKeys.has("d"))
            dx += speed * dt; // right
        if (this.#pressedKeys.has("w"))
            dy -= speed * dt; // up
        if (this.#pressedKeys.has("s"))
            dy += speed * dt; // down
        if (dx !== 0 || dy !== 0) {
            // Truck pans along camera's local X/Y axes
            this.#controls.truck(dx, dy, false);
        }
    }
    #updateOrbit(dt) {
        if (!this.#orbitActive)
            return;
        const previousState = _.cloneDeep(this.renderer.config.cameraState);
        const delta = this.#orbitSpeedDegPerSec * dt * this.#orbitDirection;
        this.renderer.updateConfig((draft) => {
            draft.cameraState.thetaOffset += delta;
        });
        this.#applyCameraStateToControls(this.renderer.config.cameraState, previousState);
        this.renderer.emit("cameraMove", this.renderer);
    }
    #handleMouseMove = (event) => {
        if (!this.#isDraggingForRotation) {
            return;
        }
        const deltaX = event.clientX - this.#lastMouseX;
        const deltaY = event.clientY - this.#lastMouseY;
        this.#lastMouseX = event.clientX;
        this.#lastMouseY = event.clientY;
        if (deltaX === 0 && deltaY === 0) {
            return;
        }
        const deltaAzimuth = deltaX * AZIMUTH_SENSITIVITY;
        const deltaPolar = -deltaY * POLAR_SENSITIVITY;
        const previousState = _.cloneDeep(this.renderer.config.cameraState);
        this.renderer.updateConfig((draft) => {
            let currentPhi = draft.cameraState.phi + deltaPolar;
            currentPhi = Math.max(1, Math.min(179, currentPhi));
            draft.cameraState.phi = currentPhi;
            draft.cameraState.thetaOffset += deltaAzimuth;
        });
        this.#applyCameraStateToControls(this.renderer.config.cameraState, previousState);
        this.renderer.emit("cameraMove", this.renderer);
    };
    #handleMouseUp = (event) => {
        if (event.button === 0 && this.#isDraggingForRotation) {
            this.#isDraggingForRotation = false;
        }
    };
    #handleMouseLeave = () => {
        if (this.#isDraggingForRotation) {
            this.#isDraggingForRotation = false;
        }
    };
    #handleTransformTreeUpdated = () => {
        this.#updateFollowFrameId();
    };
    #updateFollowFrameId() {
        const { followTf } = this.renderer.config;
        const { transformTree } = this.renderer;
        this.#updateFollowTfError();
        const followTfFrameExists = followTf != undefined && transformTree.hasFrame(followTf);
        if (followTfFrameExists) {
            this.renderer.setFollowFrameId(followTf);
            return;
        }
        const defaultFollowFrameId = transformTree.getDefaultFollowFrameId();
        this.renderer.setFollowFrameId(defaultFollowFrameId);
    }
    #updateFollowTfError = () => {
        const { followTf } = this.renderer.config;
        const { transformTree } = this.renderer;
        if (followTf != undefined) {
            if (transformTree.hasFrame(followTf)) {
                this.renderer.settings.errors.remove(FOLLOW_TF_PATH, DISPLAY_FRAME_NOT_FOUND);
            }
            else {
                this.renderer.settings.errors.add(FOLLOW_TF_PATH, DISPLAY_FRAME_NOT_FOUND, t("threeDee:frameNotFound", { frameId: followTf }));
            }
        }
        else {
            this.renderer.settings.errors.remove(FOLLOW_TF_PATH, DISPLAY_FRAME_NOT_FOUND);
        }
    };
    #handleErrorChange = () => { };
    #getUnfollowPoseSnapshot(fixedFrameId, renderFrameId, currentTime) {
        const transformTree = this.renderer.transformTree;
        if (this.#unfollowSnapshotFrameIds?.fixed !== fixedFrameId ||
            this.#unfollowSnapshotFrameIds?.render !== renderFrameId) {
            this.unfollowPoseSnapshot = makePose();
            transformTree.apply(this.unfollowPoseSnapshot, makePose(), fixedFrameId, renderFrameId, fixedFrameId, currentTime, currentTime);
            this.#unfollowSnapshotFrameIds = {
                fixed: fixedFrameId,
                render: renderFrameId,
            };
        }
        return this.unfollowPoseSnapshot;
    }
    getActiveCamera() {
        return this.renderer.config.cameraState.perspective
            ? this.#perspectiveCamera
            : this.#orthographicCamera;
    }
    handleResize(width, height, _pixelRatio) {
        this.#aspect = width / height;
        this.setCameraState(this.renderer.config.cameraState);
    }
    getCameraState() {
        return _.cloneDeep(this.renderer.config.cameraState);
    }
    setCameraState(cameraState) {
        const previousState = _.cloneDeep(this.renderer.config.cameraState);
        this.#isUpdatingCameraState = true;
        this.#updateRawCameras(cameraState);
        this.#applyCameraStateToControls(cameraState, previousState);
        this.#isUpdatingCameraState = false;
    }
    #updateRawCameras(cameraState) {
        this.#perspectiveCamera.fov = cameraState.fovy;
        this.#perspectiveCamera.near = cameraState.near;
        this.#perspectiveCamera.far = cameraState.far;
        this.#perspectiveCamera.aspect = this.#aspect;
        this.#perspectiveCamera.updateProjectionMatrix();
        if (!cameraState.perspective) {
            const orthoHeight = cameraState.distance *
                Math.tan(THREE.MathUtils.degToRad(cameraState.fovy * 0.5)) *
                2;
            const orthoWidth = orthoHeight * this.#aspect;
            this.#orthographicCamera.left = -orthoWidth / 2;
            this.#orthographicCamera.right = orthoWidth / 2;
            this.#orthographicCamera.top = orthoHeight / 2;
            this.#orthographicCamera.bottom = -orthoHeight / 2;
            this.#orthographicCamera.near = cameraState.near;
            this.#orthographicCamera.far = cameraState.far;
            this.#orthographicCamera.updateProjectionMatrix();
        }
    }
    #applyCameraStateToControls(cameraState, previousState) {
        const targetOffset = tempVec3.fromArray(cameraState.targetOffset);
        const distance = cameraState.distance;
        const phiRad = THREE.MathUtils.degToRad(cameraState.phi);
        const azimuthRad = -THREE.MathUtils.degToRad(cameraState.thetaOffset);
        const propsChanged = !previousState ||
            Math.abs(previousState.distance - distance) > 1e-6 ||
            Math.abs(previousState.phi - cameraState.phi) > 1e-6 ||
            Math.abs(previousState.thetaOffset - cameraState.thetaOffset) > 1e-6 ||
            !_.isEqual(previousState.targetOffset, cameraState.targetOffset);
        if (propsChanged) {
            tempSpherical.set(distance, phiRad, azimuthRad);
            const cameraPosition = new THREE.Vector3().setFromSpherical(tempSpherical);
            cameraPosition.add(targetOffset);
            this.#controls.setLookAt(cameraPosition.x, cameraPosition.y, cameraPosition.z, targetOffset.x, targetOffset.y, targetOffset.z, false);
            this.#lastTargetOffset.copy(targetOffset);
        }
        if (cameraState.perspective) {
            this.#controls.minPolarAngle = 0;
            this.#controls.maxPolarAngle = Math.PI;
        }
        else {
            this.#controls.minPolarAngle = 0;
            this.#controls.maxPolarAngle = Math.PI;
            this.#controls.getPosition(this.#orthographicCamera.position);
            this.#controls.getTarget(tempVec3);
            this.#orthographicCamera.lookAt(tempVec3);
            this.#orthographicCamera.updateMatrixWorld();
        }
        this.#controls.update(0);
    }
}
//# sourceMappingURL=CameraStateSettings.js.map