1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Scene } from '@babylonjs/core/scene';
import { KeyboardEventTypes, PointerEventTypes, type IKeyboardEvent } from '@babylonjs/core/Events';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
import { Color3 } from '@babylonjs/core/Maths/math.color'
import { Animation } from '@babylonjs/core/Animations/animation';
import { QuadraticEase, EasingFunction } from '@babylonjs/core/Animations/easing';
import { AdomStatics } from '../AdomStatics';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { writable, get } from 'svelte/store';

/** When true, disables camera keyboard input (WASD). Use when another control has focus (e.g. input fields). */
export const disableCameraKeyboardStore = writable<boolean>(false);


export class AdomCamera extends ArcRotateCamera{

    // State for keyboard and mouse handling
    private isPointerDownWithShiftAlt: boolean = false;
    private lastCamTarget: Vector3 | null = null;
    private lastCamTargetMove: any;
    /** Pivot point picked by shift+alt / click-to-set; snapped to on early pointer-up. */
    private pendingPivotTarget: Vector3 | null = null;
    private sphere: any;
    private sphereHighlightLayer: HighlightLayer | undefined;
    private isRotating: boolean = false;
    private sphereVisibilityThreshold: number = 500; // ms to keep sphere visible after rotation stops
    // Controls whether normal left-clicks behave like shift+alt+click
    private useClickToSetRotationPoint: boolean = false;
    private clickStartTime: number = 0; // To track when the click started
    private clickDelayTimeout: NodeJS.Timeout | null = null; // For delaying click-to-set behavior
    private clickDelayDuration: number = 500; // 0.5 second delay for click-to-set (reduced from 1 second)
    // Orbiting with spacebar properties
    private isSpacebarPressed: boolean = false;
    private orbitAnimationFrameId: number | null = null;
    private orbitSpeed: number = 0.005; // radians per frame
    // First-person movement properties
    private moveSpeed: number = 2;
    private keysPressed: { [key: string]: boolean } = {
        'w': false,
        's': false,
        'a': false,
        'd': false,
        'q': false,
        'e': false
    };
    // Precision mode (toggle with Caps Lock)
    private precisionMode: boolean = false;
    // Base speeds (normal mode)
    private baseOrbitSpeed: number = 0.005;
    private baseMoveSpeed: number = 2;
    // Camera bounds properties
    private targetBounds: {
        minX: number | null;
        maxX: number | null;
        minY: number | null;
        maxY: number | null;
        minZ: number | null;
        maxZ: number | null;
    } = {
        minX: null,
        maxX: null,
        minY: null,
        maxY: null,
        minZ: null,
        maxZ: null
    };
    private enforceBounds: boolean = false;

    // Camera rotation detection for middle mouse
    private lastAlpha: number = 0;
    private lastBeta: number = 0;
    private lastRadius: number = 0;
    private isMiddleMouseRotating: boolean = false;
    private rotationDetectionTimeout: NodeJS.Timeout | null = null;
    private isMiddleMouseDown: boolean = false;
    
    // Pinch zoom sensitivity (multiplier for trackpad pinch gestures)
    private pinchZoomMultiplier: number = 1.0;
    private canvas: HTMLCanvasElement | null = null;
    private wheelHandler: ((e: WheelEvent) => void) | null = null;
    
    // Right-click drag panning
    private isRightMouseDown: boolean = false;
    private isCtrlLeftPanning: boolean = false;
    private lastPointerX: number = 0;
    private lastPointerY: number = 0;
    private panSpeed: number = 1.0;

    // Event listeners for external control (e.g. automated tests or external UI)
    private externalEventListeners: Map<string, EventListener> = new Map();

    /***
     * Create a new AdomCamera
     * @param scene: The scene that the camera is in
     * @param canvas: The canvas that the camera is in
     * @param zUp: If true (default), Z-axis is up. If false, Y-axis is up (Babylon default).
    */
    constructor(scene: Scene, canvas: HTMLCanvasElement, zUp: boolean = true) {
        // console.log("adom camera start");
        // Create the camera
        super("adomCamera", 0, 0, 2500, new Vector3(0, 0, 140), scene);

        this.upVector = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0);
        this.canvas = canvas;
        this.attachControl(canvas, true);
        this.parent = null;

        // Additional AdomCamera initialization
        this.setupCameraDefaults();
        this.setupKeyboardEvents();
        this.setupSphereAndHighlight();
        this.setupPointerEvents();
        this.setupPinchZoom();
        
        // Initialize rotation detection values AFTER camera setup
        this.initializeRotationDetection();
        
        this.setupExternalEventListeners();
        // console.log("adom camera end");

    }

    /***
    * Getter to expose the ArcRotateCamera
    */
    public getCamera(): ArcRotateCamera {
        return this;
    }

    /***
    * Get the current coordinates of the camera
    */
    public getCameraCoords() {
        // console.log("Camera position:", this.position);
        // console.log("Camera target:", this.target);
        // console.log("Camera radius:", this.radius);
        // console.log("Camera alpha:", this.alpha);
        // console.log("Camera beta:", this.beta);

        let str = 
        `Camera.moveCamTo(
        0, 0, 0, // Position values are ignored - will be calculated from other parameters
        ${this.target.x}, ${this.target.y}, ${this.target.z}, // target - what the camera looks at
        ${this.radius}, ${this.alpha}, ${this.beta}, // radius, alpha, beta - determine camera position
        60, // Optional: fps (defaults to 60)
        60); // Optional: totalframes (defaults to 60)`;

        navigator.clipboard?.writeText(str);
        
        // console.log("Camera coordinates copied to clipboard");
        return str;
    }

    /***
    * Set up the camera to work in our environment
    */
    private setupCameraDefaults() {
        this.allowUpsideDown = true;
        this.panningSensibility = 0; // Disable built-in panning (0 = disabled in Babylon)
        this.panningInertia = 0;
        this.invertRotation = false;
        this.panningAxis = new Vector3(1, 1, 0);
        this.inertia = 0.1;
        this.angularSensibilityX = 500;
        this.angularSensibilityY = 500;
        this.lowerAlphaLimit = -100;
        this.lowerBetaLimit = -100;
        this.lowerRadiusLimit = -100;
        this.upperRadiusLimit = 2700;
        this.wheelDeltaPercentage = 0.1;
        // minZ is set here, maxZ is left at Babylon default for perspective mode
        // Orthographic mode dynamically adjusts both in updateClippingPlanes()
        // radius/50 (not /1000): the near plane is the depth-precision lever.
        // PCB exports stack layers 10-40 microns apart; with near at
        // radius/1000 the depth quantum at working distance is ~20 microns
        // and coplanar-ish layers z-fight (moire on the mask around vias).
        // The quantum grows with distance squared, so max zoom-out needs the
        // extra headroom of /50 over /100.
        this.minZ = this.radius / 50;
        // Note: NOT setting maxZ here - Babylon's default works better for perspective
        // upVector already set in constructor from zUp option
        this.alpha = Math.PI / 2;
        this.beta = Math.PI / 4;
        this.zoomToMouseLocation = true;
        
        // Load camera mode from localStorage, default to perspective.
        // Guarded: a sandboxed iframe without allow-same-origin throws
        // SecurityError on ANY storage access (even a read), and an
        // exception here silently kills the whole scene rig (lights, env,
        // ViewCube). Sandboxed embeds just get the default mode.
        let savedMode: string | null = null;
        try {
            savedMode = typeof window !== 'undefined' ? window.localStorage.getItem('adom_camera_mode') : null;
        } catch (e) {
            savedMode = null;
        }
        if (savedMode === 'orthographic') {
            this.mode = ArcRotateCamera.ORTHOGRAPHIC_CAMERA;
            this.wasOrthoMode = true; // Initialize tracking flag
            // console.log('Camera initialized in Orthographic mode (from localStorage)');
        } else {
            this.mode = ArcRotateCamera.PERSPECTIVE_CAMERA;
            // console.log('Camera initialized in Perspective mode');
        }
        // Initial orthographic bounds - these will be updated based on radius
        this.updateOrthographicBounds();
        
        // Register an observer to check bounds on every frame
        this._scene.onBeforeRenderObservable.add(() => {
            if (this.enforceBounds) {
                this.checkAndEnforceBounds();
            }
            
            // Handle keyboard movement
            this.processKeyboardInput();
            
            // Detect middle mouse rotation and show orbit sphere
            this.detectAndShowRotationSphere();
            
            // Update orthographic bounds and clipping planes based on radius for proper zooming
            this.updateClippingPlanes();
        });
    }

    /**
     * Update orthographic bounds based on camera radius for proper zooming
     */
    public updateOrthographicBounds(): void {
        // Scale orthographic bounds based on radius
        // Smaller radius = more zoomed in (smaller bounds)
        const scale = this.radius / 2;
        const aspect = this._scene.getEngine().getRenderWidth() / this._scene.getEngine().getRenderHeight();
        
        this.orthoLeft = -scale * aspect;
        this.orthoRight = scale * aspect;
        this.orthoTop = scale;
        this.orthoBottom = -scale;
    }
    
    // Track whether we've set ortho clipping to know when to restore perspective values
    private wasOrthoMode: boolean = false;
    
    /**
     * Update clipping planes based on camera mode
     * Orthographic mode requires different clipping plane calculations to prevent
     * excessive near-plane clipping while maintaining good z-buffer precision
     */
    private updateClippingPlanes(): void {
        if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {
            this.updateOrthographicBounds();
            // In orthographic mode, use negative minZ to prevent near-plane clipping
            // when viewing at angles. Objects "in front" of the camera plane need to be visible.
            this.minZ = -this.radius * 20;
            this.maxZ = this.radius * 20;
            this.wasOrthoMode = true;
        } else {
            if (this.wasOrthoMode) {
                this.wasOrthoMode = false;
            }
            // Scale minZ with radius to maintain good depth buffer precision at all zoom levels
            // (radius/50, not /1000 — see setupCameraDefaults; 20x depth precision)
            this.minZ = this.radius / 50;
        }
    }

    /***
    * Set up key inputs for our environment
    */
    private setupKeyboardEvents() {
        this._scene.onKeyboardObservable.add((kbInfo) => {
            switch (kbInfo.type) {
                case KeyboardEventTypes.KEYDOWN:
                    this.handleKeyDown(kbInfo.event);
                    break;
                case KeyboardEventTypes.KEYUP:
                    this.handleKeyUp(kbInfo.event);
                    break;
            }
        });
    }

    /***
    * Helper for keyboard events
    */
    private handleKeyDown(event: IKeyboardEvent) {
        const key = event.key.toLowerCase();
        const code = event.code;
        
        // Toggle precision mode on Caps Lock press
        if (code === 'CapsLock') {
            this.precisionMode = !this.precisionMode;
            this.updateSpeeds();
            return;
        }
        
        if (get(disableCameraKeyboardStore)) {
            return;
        }
        
        // Handle spacebar for orbit
        if (key === ' ' || code === 'Space') {
            if (!this.isSpacebarPressed) {
                this.isSpacebarPressed = true;
                this.startOrbiting();
            }
            return;
        }
        
        // Track WASD key presses
        if (key in this.keysPressed) {
            this.keysPressed[key] = true;
        }
    }

    private handleKeyUp(event: IKeyboardEvent) {
        const key = event.key.toLowerCase();
        const code = event.code;
        
        if (key === ' ' || code === 'Space') {
            this.isSpacebarPressed = false;
            this.stopOrbiting();
            return;
        }
        
        // Track WASD key releases
        if (key in this.keysPressed) {
            this.keysPressed[key] = false;
        }
    }
    
    /**
     * Updates movement and orbit speeds based on precision mode
     */
    private updateSpeeds(): void {
        if (this.precisionMode) {
            // Precision mode (1/4 speed)
            this.orbitSpeed = this.baseOrbitSpeed / 4;
            this.moveSpeed = this.baseMoveSpeed / 4;
            // console.log('Precision mode activated: speed reduced to 25%');
        } else {
            // Normal speed
            this.orbitSpeed = this.baseOrbitSpeed;
            this.moveSpeed = this.baseMoveSpeed;
            // console.log('Normal speed restored');
        }
    }

    /**
     * Start orbiting around the target point
     */
    private startOrbiting() {
        // Show the rotation sphere at the target position
        this.showRotationSphere();

        // Start the animation if it's not already running
        if (this.orbitAnimationFrameId === null) {
            // Use requestAnimationFrame for smooth animation
            const animate = () => {
                if (this.isSpacebarPressed) {
                    // Increment alpha to orbit around the target
                    this.alpha += this.orbitSpeed;
                    
                    // Keep sphere visible and properly sized
                    this.updateRotationSphere();
                    
                    // Continue animation
                    this.orbitAnimationFrameId = requestAnimationFrame(animate);
                }
            };
            
            this.orbitAnimationFrameId = requestAnimationFrame(animate);
        }
    }

    /**
     * Stop orbiting animation
     */
    private stopOrbiting() {
        // Cancel the animation frame
        if (this.orbitAnimationFrameId !== null) {
            cancelAnimationFrame(this.orbitAnimationFrameId);
            this.orbitAnimationFrameId = null;
        }
        
        // Hide the rotation sphere
        if (this.sphere) {
            this.sphere.isVisible = false;
        }
    }

    /**
     * Set the orbit speed 
     * @param speed The speed in radians per frame
     */
    public setOrbitSpeed(speed: number): void {
        this.baseOrbitSpeed = speed;
        // Apply precision mode slowdown if active
        this.orbitSpeed = this.precisionMode ? speed / 4 : speed;
    }

    /**
     * Get the current orbit speed
     */
    public getOrbitSpeed(): number {
        return this.orbitSpeed;
    }

    /**
     * Initialize rotation detection values to current camera state
     */
    private initializeRotationDetection(): void {
        this.lastAlpha = this.alpha;
        this.lastBeta = this.beta;
        this.lastRadius = this.radius;
        this.isMiddleMouseRotating = false;
        this.isMiddleMouseDown = false;
        
        // Make sure sphere is hidden initially
        if (this.sphere) {
            this.sphere.isVisible = false;
        }
        // console.log("Rotation detection initialized");
    }
    
    /**
     * Set up event listeners for external control (e.g. automated tests or external UI).
     * Trigger via window.dispatchEvent(new CustomEvent('camera:change_view', { detail: { orientation: 'top' } })).
     */
    private setupExternalEventListeners(): void {
        // Create a single event listener that handles all orientations
        const changeViewListener = (event: Event) => {
            const customEvent = event as CustomEvent;
            const orientation = customEvent.detail?.orientation;
            
            if (!orientation) {
                // console.error('camera:change_view event requires orientation in detail');
                return;
            }
            
            switch (orientation) {
                case 'top':
                    this.rotateToCubeTop();
                    break;
                case 'top_front_right':
                    this.rotateToCubeTopFrontRight();
                    break;
                case 'top_front_left':
                    this.rotateToCubeTopFrontLeft();
                    break;
                case 'top_back_right':
                    this.rotateToCubeTopBackRight();
                    break;
                case 'top_back_left':
                    this.rotateToCubeTopBackLeft();
                    break;
                default:
                    // console.error(`Unknown camera orientation: ${orientation}`);
            }
        };
        
        // Store listener for cleanup
        this.externalEventListeners.set('camera:change_view', changeViewListener as EventListener);
        
        // Add event listener to window
        if (typeof window !== 'undefined') {
            window.addEventListener('camera:change_view', changeViewListener as EventListener);
            // console.log('Camera external event listener registered for camera:change_view');
        }
    }

    /**
     * Reset middle mouse detection state to ensure clean detection
     */
    private resetMiddleMouseState(): void {
        // console.log("Resetting middle mouse state for clean detection");
        this.isMiddleMouseRotating = false;
        // Don't reset this.isMiddleMouseDown as it tracks actual button state
        
        // Clear finished animations using robust checking
        if (this.lastCamTargetMove) {
            const animatable = this.lastCamTargetMove;
            const shouldClear = animatable.animationEnded || 
                              !animatable.animationStarted ||
                              !animatable.runtimeAnimations ||
                              animatable.runtimeAnimations.length === 0 ||
                              animatable.runtimeAnimations.every((anim: any) => !anim.animation || anim.animation._stopped);
            
            if (shouldClear) {
                // console.log("Clearing finished animation during reset");
                this.lastCamTargetMove = null;
            }
        }
        
        // Update last known camera values to current state
        this.lastAlpha = this.alpha;
        this.lastBeta = this.beta;
        this.lastRadius = this.radius;
    }

    /**
     * Detect camera rotation (middle mouse) and show/hide rotation sphere accordingly
     */
    private detectAndShowRotationSphere(): void {
        const currentAlpha = this.alpha;
        const currentBeta = this.beta;
        const currentRadius = this.radius;
        
        // Check if camera rotation values have changed significantly
        const alphaChanged = Math.abs(currentAlpha - this.lastAlpha) > 0.001;
        const betaChanged = Math.abs(currentBeta - this.lastBeta) > 0.001;
        const radiusChanged = Math.abs(currentRadius - this.lastRadius) > 0.1;
        
        const isRotatingNow = alphaChanged || betaChanged || radiusChanged;
        
        // Check if animation is actually still running
        let isAnimationRunning = false;
        if (this.lastCamTargetMove) {
            // Check multiple properties to determine if animation is truly finished
            const animatable = this.lastCamTargetMove;
            isAnimationRunning = animatable.animationStarted && 
                               !animatable.animationEnded && 
                               animatable.runtimeAnimations && 
                               animatable.runtimeAnimations.length > 0 &&
                               animatable.runtimeAnimations.some((anim: any) => anim.animation && !anim.animation._stopped);
        }
        
        const canDetectMiddleMouse = !this.isSpacebarPressed && 
                                   !this.isPointerDownWithShiftAlt && 
                                   !this.isRotating && 
                                   !isAnimationRunning;
        
        if (this.isMiddleMouseDown && !this.isMiddleMouseRotating && canDetectMiddleMouse) {
            // Check if camera is rotating significantly
            if (isRotatingNow) {
                this.isMiddleMouseRotating = true;
                this.showRotationSphere();
                // console.log("Middle mouse rotation detected - showing orbit sphere");
            }
        } else if (this.isMiddleMouseDown && !this.isMiddleMouseRotating && !canDetectMiddleMouse) {
            // Debug: log what's blocking detection
            // console.log("Middle mouse blocked - spacebar:", this.isSpacebarPressed, 
            //            "shiftAlt:", this.isPointerDownWithShiftAlt,
            //            "leftRotating:", this.isRotating,
            //            "animation:", !!this.lastCamTargetMove,
            //            "animRunning:", isAnimationRunning);
        }
        
        // Clean up finished animations - use more robust checking
        if (this.lastCamTargetMove) {
            const animatable = this.lastCamTargetMove;
            const shouldClear = animatable.animationEnded || 
                              !animatable.animationStarted ||
                              !animatable.runtimeAnimations ||
                              animatable.runtimeAnimations.length === 0 ||
                              animatable.runtimeAnimations.every((anim: any) => !anim.animation || anim.animation._stopped);
            
            if (shouldClear) {
                // console.log("Clearing finished camera animation");
                this.lastCamTargetMove = null;
            }
        }
        
        // Handle middle mouse rotation sphere display
        if (this.isMiddleMouseRotating) {
            if (this.isMiddleMouseDown) {
                // Keep updating sphere position during rotation
                this.updateRotationSphere();
                
                // Clear any pending timeout since we're still rotating
                if (this.rotationDetectionTimeout) {
                    clearTimeout(this.rotationDetectionTimeout);
                    this.rotationDetectionTimeout = null;
                }
            } else {
                // Middle mouse was released - hide sphere immediately
                this.isMiddleMouseRotating = false;
                if (this.sphere) {
                    this.sphere.isVisible = false;
                }
                // console.log("Middle mouse released - hiding orbit sphere");
            }
        }
        
        // Update last known values
        this.lastAlpha = currentAlpha;
        this.lastBeta = currentBeta;
        this.lastRadius = currentRadius;
    }

    /***
    * Glowing sphere for showing where the rotation axis is
    */
    private setupSphereAndHighlight() {
        // Create a sphere to show the point where the camera is rotating around
        this.sphere = MeshBuilder.CreateSphere('arcRotateIndicatorSphere', { segments: 10, diameter: 1 }, this._scene);
        this.sphere.isVisible = false;

        const material = new StandardMaterial("rotationSphereMaterial", this._scene);
        material.emissiveColor = new Color3(0, 0.38, 0.94);
        this.sphere.material = material;
    }

    /***
    * Set up pointer event inputs for our environment
    */
    private setupPointerEvents() {
        let rotationTimeout: NodeJS.Timeout | null = null;

        this._scene.onPointerObservable.add((pointerInfo) => {
            switch (pointerInfo.type) {
                case PointerEventTypes.POINTERDOWN:
                    // Track right mouse button for custom panning
                    if (pointerInfo.event.button === 2) {
                        this.isRightMouseDown = true;
                        this.lastPointerX = pointerInfo.event.clientX;
                        this.lastPointerY = pointerInfo.event.clientY;
                    }
                    // Track middle mouse button
                    if (pointerInfo.event.button === 1) { // Middle mouse button
                        this.isMiddleMouseDown = true;
                    }
                    if (pointerInfo.event.shiftKey && pointerInfo.event.altKey) {
                        // Traditional shift+alt+click behavior (immediate)
                        this.isPointerDownWithShiftAlt = true;
                        const pickResult = pointerInfo.pickInfo;
                        if (pickResult?.hit) {
                            const clickedPoint = pickResult.pickedPoint;
                            if (clickedPoint) {
                                this.handleShiftAltClick(clickedPoint);
                            }
                        }
                    } else if (pointerInfo.event.button === 0 && pointerInfo.event.ctrlKey) {
                        // Ctrl+Left-click: custom panning (matches right-click speed)
                        this.isCtrlLeftPanning = true;
                        this.lastPointerX = pointerInfo.event.clientX;
                        this.lastPointerY = pointerInfo.event.clientY;
                    } else if (pointerInfo.event.button === 0) { // Left mouse button
                        if (this.useClickToSetRotationPoint) {
                            // Store the time when the click started
                            this.clickStartTime = performance.now();
                            // Clear any existing timeout
                            if (this.clickDelayTimeout) {
                                clearTimeout(this.clickDelayTimeout);
                                this.clickDelayTimeout = null;
                            }
                            
                            // Set up a timeout for the delayed click-to-set behavior
                            this.clickDelayTimeout = setTimeout(() => {
                                // Only proceed if we're still in the same click (mouse button still down)
                                if (this.isRotating) {
                                    const pickResult = pointerInfo.pickInfo;
                                    if (pickResult?.hit) {
                                        const clickedPoint = pickResult.pickedPoint;
                                        if (clickedPoint) {
                                            // Handle the delayed click to set rotation point
                                            this.handleClick(clickedPoint);
                                        }
                                    }
                                }
                                this.clickDelayTimeout = null;
                            }, this.clickDelayDuration);
                            
                            // Mark that we're rotating - this will be used for regular rotation until timeout
                            this.isRotating = true;
                            this.showRotationSphere();
                        } else {
                            // Left-click: orbit only (do not set rotation point); show sphere like middle-mouse
                            this.isRotating = true;
                            this.showRotationSphere();
                        }
                    }
                    break;
                case PointerEventTypes.POINTERMOVE:
                    // Custom drag panning (right-click or ctrl+left-click)
                    if (this.isRightMouseDown || this.isCtrlLeftPanning) {
                        const dx = pointerInfo.event.clientX - this.lastPointerX;
                        const dy = pointerInfo.event.clientY - this.lastPointerY;
                        this.lastPointerX = pointerInfo.event.clientX;
                        this.lastPointerY = pointerInfo.event.clientY;

                        // Get camera right and up vectors in world space
                        const rightDir = this.getDirection(Vector3.Right());
                        const upDir = this.getDirection(Vector3.Up());

                        // Scale pan amount by radius so panning feels consistent at any zoom
                        const scale = (this.radius / 1000) * this.panSpeed;
                        const offset = rightDir.scale(-dx * scale).add(upDir.scale(dy * scale));

                        this.target.addInPlace(offset);
                    }
                    if (this.isRotating && !this.isPointerDownWithShiftAlt) {
                        // Keep sphere visible and update its position during rotation
                        // Only update sphere position in normal mode, not in click-to-set mode
                        if (!this.useClickToSetRotationPoint || this.clickDelayTimeout === null) {
                            this.updateRotationSphere();
                        }

                        // Clear any existing timeout
                        if (rotationTimeout) {
                            clearTimeout(rotationTimeout);
                            rotationTimeout = null;
                        }
                    }
                    break;
                case PointerEventTypes.POINTERUP:
                    // Track right mouse button release
                    if (pointerInfo.event.button === 2) {
                        this.isRightMouseDown = false;
                    }
                    // Track ctrl+left-click panning release
                    if (pointerInfo.event.button === 0 && this.isCtrlLeftPanning) {
                        this.isCtrlLeftPanning = false;
                    }
                    // Track middle mouse button release
                    if (pointerInfo.event.button === 1) { // Middle mouse button
                        this.isMiddleMouseDown = false;
                    }
                    
                    if (this.isPointerDownWithShiftAlt) {
                        this.isPointerDownWithShiftAlt = false;
                        this._resetCameraTarget();
                        
                        // Reset middle mouse state for clean detection
                        // console.log("Shift+Alt interaction ended - resetting middle mouse state");
                        this.resetMiddleMouseState();
                    }

                    if (this.isRotating && pointerInfo.event.button === 0) { // Only reset for left mouse button
                        this.isRotating = false;

                        // Cancel the delayed click-to-set behavior on pointer up
                        if (this.clickDelayTimeout) {
                            clearTimeout(this.clickDelayTimeout);
                            this.clickDelayTimeout = null;
                        }

                        // Hide the sphere on mouse up only if it was shown by left-click rotation
                        // Don't hide if we're currently orbiting with spacebar or middle mouse rotating
                        if (this.sphere && !this.isSpacebarPressed && !this.isMiddleMouseRotating) {
                            this.sphere.isVisible = false;
                        }

                        // Clean up any pending timeout
                        if (rotationTimeout) {
                            clearTimeout(rotationTimeout);
                            rotationTimeout = null;
                        }
                        
                        // console.log("Left mouse released - reset isRotating, ready for middle mouse");
                        
                        // Reset middle mouse state for clean detection
                        this.resetMiddleMouseState();
                    }
                    break;
            }
        });
    }

    /**
     * Set up enhanced pinch zoom for trackpad gestures
     * Trackpad pinch sends wheel events with ctrlKey=true and smaller deltaY values
     * Implements zoom-to-cursor like zoomToMouseLocation does for regular wheel
     */
    private setupPinchZoom() {
        if (!this.canvas) return;

        this.wheelHandler = (e: WheelEvent) => {
            // Always consume the wheel so it zooms the scene instead of
            // scrolling the embedding page (the viewer is iframed into wiki
            // pages). Regular wheel zoom is handled by Babylon's built-in
            // mousewheel input, which still runs after this preventDefault;
            // the ctrlKey branch below adds boosted trackpad-pinch zoom.
            if (e.cancelable) e.preventDefault();

            // Only handle pinch gestures (ctrlKey is true for trackpad pinch)
            if (e.ctrlKey) {
                // Apply boosted zoom for pinch gestures
                // Negative deltaY = pinch out (zoom in), positive = pinch in (zoom out)
                const zoomDelta = -e.deltaY * this.pinchZoomMultiplier;
                const zoomFactor = 1 + (zoomDelta * 0.01);
                
                // Calculate new radius
                let newRadius = this.radius / zoomFactor;
                
                // Respect radius limits
                if (this.lowerRadiusLimit !== null && newRadius < this.lowerRadiusLimit) {
                    newRadius = this.lowerRadiusLimit;
                } else if (this.upperRadiusLimit !== null && newRadius > this.upperRadiusLimit) {
                    newRadius = this.upperRadiusLimit;
                }
                
                // Zoom to mouse location (like zoomToMouseLocation for regular wheel)
                if (this.zoomToMouseLocation && this._scene) {
                    const radiusChange = this.radius - newRadius;
                    
                    // Get mouse position relative to canvas
                    const rect = this.canvas!.getBoundingClientRect();
                    const mouseX = e.clientX - rect.left;
                    const mouseY = e.clientY - rect.top;
                    
                    // Create a ray from the mouse position
                    const ray = this._scene.createPickingRay(
                        mouseX, 
                        mouseY, 
                        null, 
                        this, 
                        false
                    );
                    
                    // In orthographic mode the ray origin is on the near plane (not the camera),
                    // so we must intersect the ray with the plane through the target (perpendicular
                    // to view) to get the correct point under the cursor. In perspective, ray.origin
                    // is the camera position so distanceAlongRay = distanceToTarget is correct.
                    let pointAtTarget: Vector3;
                    if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {
                        const viewDir = ray.direction;
                        const t = Vector3.Dot(this.target.subtract(ray.origin), viewDir);
                        pointAtTarget = ray.origin.add(viewDir.scale(t));
                    } else {
                        const distanceToTarget = Vector3.Distance(this.position, this.target);
                        pointAtTarget = ray.origin.add(ray.direction.scale(distanceToTarget));
                    }
                    
                    // Calculate how much to move the target toward the mouse point
                    // The amount is proportional to the zoom change
                    const moveRatio = radiusChange / this.radius;
                    const targetOffset = pointAtTarget.subtract(this.target).scale(moveRatio);
                    
                    // Apply the target offset
                    this.target.addInPlace(targetOffset);
                }
                
                // Apply the new radius
                this.radius = newRadius;
            }
        };

        this.canvas.addEventListener('wheel', this.wheelHandler, { passive: false });
    }

    /**
     * Set the pinch zoom sensitivity multiplier
     * Higher values = faster zoom on trackpad pinch
     * @param multiplier The multiplier (default is 3.0, range 1.0-10.0 recommended)
     */
    public setPinchZoomMultiplier(multiplier: number): void {
        this.pinchZoomMultiplier = Math.max(0.5, Math.min(20, multiplier));
    }

    /**
     * Get the current pinch zoom multiplier
     */
    public getPinchZoomMultiplier(): number {
        return this.pinchZoomMultiplier;
    }

    /**
     * Show the rotation sphere at the current target position
     */
    private showRotationSphere() {
        if (!this.isPointerDownWithShiftAlt) {
            this.sphere.position = this.target.clone();
            this.sphere.isVisible = true;
            const scale = (this.radius / 100);
            this.sphere.scaling = new Vector3(scale, scale, scale);
        }
    }

    /**
     * Update the rotation sphere position and scale
     */
    private updateRotationSphere() {
        if (!this.isPointerDownWithShiftAlt && this.sphere) {
            this.sphere.position = this.target.clone();
            const scale = (this.radius / 100);
            this.sphere.scaling = new Vector3(scale, scale, scale);
        }
    }

    /***
    * Move camera to position and add the glowing sphere
    * Show the user the new rotation axis point
    */
    private handleShiftAltClick(clickedPoint: Vector3) {
        const tempRadius = this.radius;

        // Store the previous target
        this.lastCamTarget = this.target.clone();
        this.pendingPivotTarget = clickedPoint.clone();

        // Show the sphere at the clicked point
        this.sphere.position = clickedPoint;
        this.sphere.isVisible = true;
        const scale = (this.radius / 100) * 2;
        this.sphere.scaling = new Vector3(scale, scale, scale);

        // Animate the camera to the clicked point
        this.lastCamTargetMove = this.moveCamTargetTo(clickedPoint._x, clickedPoint._y, clickedPoint._z, 120, 60);

        // Preserve the camera radius
        this.radius = tempRadius;
    }

    /**
     * Handle a mouse click when in click-to-set-rotation mode
     * Similar to handleShiftAltClick but designed for regular clicks
     */
    private handleClick(clickedPoint: Vector3) {
        const tempRadius = this.radius;

        // Store the previous target
        this.lastCamTarget = this.target.clone();
        this.pendingPivotTarget = clickedPoint.clone();

        // Show the sphere at the clicked point
        this.sphere.position = clickedPoint;
        this.sphere.isVisible = true;
        const scale = (this.radius / 100);
        this.sphere.scaling = new Vector3(scale, scale, scale);

        // Animate the camera to the clicked point
        this.lastCamTargetMove = this.moveCamTargetTo(clickedPoint._x, clickedPoint._y, clickedPoint._z, 120, 60);

        // Preserve the camera radius
        this.radius = tempRadius;
    }

    /***
    * Reset the camera's target
    */
    private _resetCameraTarget() {
        // Hide the sphere only if it's not being used for middle mouse rotation or spacebar orbiting
        if (!this.isMiddleMouseRotating && !this.isSpacebarPressed) {
            this.sphere.isVisible = false;
        }

        // Stop and clear the animation if it exists
        if (this.lastCamTargetMove) {
            this.lastCamTargetMove.stop();
            this.lastCamTargetMove = null;
            // A quick click releases the pointer before the 0.5s target glide
            // finishes; stopping mid-flight would leave the orbit center
            // partway between old and new. Snap to the picked point instead.
            if (this.pendingPivotTarget) {
                this.target = this.pendingPivotTarget.clone();
            }
        }
        this.pendingPivotTarget = null;
    }

    /***
     * Move the camera to its new target
     * @param x :number The x position of the target
     * @param y :number The y position of the target
     * @param z :number The z position of the target
     * @param fps :number frames per second (defaults to 60)
     * @param totalFrames :number total frames of the animation (this/fps = length of animation)
    */
    public moveCamTargetTo(x: number, y: number, z: number, fps: number = 60, totalFrames: number = 60): any {
        // console.log("moving camera");
        const animTarget = new Animation('target', 'target', fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
        const easingFunction = new QuadraticEase();
        easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);

        animTarget.setKeys([
            { frame: 0, value: this.target },
            { frame: totalFrames, value: new Vector3(x, y, z) }
        ]);
        this.animations.push(animTarget);

        const animatable = this._scene.beginAnimation(this, 0, totalFrames, false);
        
        // Add a safety timeout to ensure animation reference is cleared
        // Duration = (totalFrames / fps) * 1000ms + 100ms buffer
        const animationDuration = (totalFrames / fps) * 1000 + 100;
        setTimeout(() => {
            if (this.lastCamTargetMove === animatable) {
                // console.log("Safety timeout: clearing animation reference");
                this.lastCamTargetMove = null;
            }
        }, animationDuration);
        
        return animatable;
    }


    /***
     * Instantly move the camera to this new spot
     * @param x :number The x position of the camera (ignored - derived from other parameters)
     * @param y :number The y position of the camera (ignored - derived from other parameters)
     * @param z :number The z position of the camera (ignored - derived from other parameters)
     * @param tx :number The x position of the camera target
     * @param ty :number The y position of the camera target
     * @param tz :number The z position of the camera target
     * @param r :number The radius of the camera
     * @param a :number The alpha of the camera
     * @param b :number The beta of the camera
    */
    public teleportCamTo(x: number, y: number, z: number, tx: number, ty: number, tz: number, r: number, a: number, b: number) {
        // In an ArcRotateCamera, position is derived from target, radius, alpha, and beta
        // So we don't need to set position directly - it'll be computed automatically
        this.target = new Vector3(tx, ty, tz);
        this.radius = r;
        this.alpha = a;
        this.beta = b;
        
        // The camera will update position automatically
        this.rebuildAnglesAndRadius();
    }

    /***
     * Move the camera slowly to a new position and target with optional easing function.
     * @param x :number The x position of the camera (will be calculated based on target, radius, alpha, beta)
     * @param y :number The y position of the camera (will be calculated based on target, radius, alpha, beta)
     * @param z :number The z position of the camera (will be calculated based on target, radius, alpha, beta)
     * @param tx :number The x position of the camera target
     * @param ty :number The y position of the camera target
     * @param tz :number The z position of the camera target
     * @param r :number The radius of the camera
     * @param a :number The alpha of the camera
     * @param b :number The beta of the camera
     * @param fps :number Optional Defaults to 60. The frames per second of the animation
     * @param totalFrames :number Optional Defaults to 60. The total frames of the animation
     * @param easingFunction :EasingFunction Optional Defaults to QuadraticEase. The easing function to use
    */
    public async moveCamTo(x: number, y: number, z: number, tx: number, ty: number, tz: number,
        r: number, a: number, b: number, fps:number =60, totalFrames:number =60,
        easingFunction = new QuadraticEase()) {

        // For each easing function, you can choose between EASEIN (default), EASEOUT, EASEINOUT
        easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);

        // For ArcRotateCamera, we want to ignore the position input and focus on target, radius, alpha, beta
        // Input position parameters are ignored, since position is derived from target, radius, alpha, and beta
        
        // Animate target
        const animTarget = new Animation('target', 'target', fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
        animTarget.setEasingFunction(easingFunction);
        animTarget.setKeys([
            { frame: 0, value: this.target },
            { frame: totalFrames, value: new Vector3(tx, ty, tz) }
        ]);
        this.animations.push(animTarget);

        // Animate radius
        const animRadius = new Animation('radius', 'radius', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);
        animRadius.setEasingFunction(easingFunction);
        animRadius.setKeys([
            { frame: 0, value: this.radius },
            { frame: totalFrames, value: r }
        ]);
        this.animations.push(animRadius);

        // Animate alpha (horizontal rotation)
        const animAlpha = new Animation('alpha', 'alpha', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);
        animAlpha.setEasingFunction(easingFunction);
        animAlpha.setKeys([
            { frame: 0, value: this.alpha },
            { frame: totalFrames, value: a }
        ]);
        this.animations.push(animAlpha);

        // Animate beta (vertical rotation)
        const animBeta = new Animation('beta', 'beta', fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CONSTANT);
        animBeta.setEasingFunction(easingFunction);
        animBeta.setKeys([
            { frame: 0, value: this.beta },
            { frame: totalFrames, value: b }
        ]);
        this.animations.push(animBeta);

        if (AdomStatics.scene) {
            // console.log("Started moveCamTo anim");
            const anim = AdomStatics.scene.beginAnimation(this, 0, totalFrames, false);
            await anim.waitAsync();
            // console.log("Finished moveCamTo anim");
        }
    }

    /**
     * Dispose of the camera (for cleanup).
     */
    public dispose() {
        // Stop any running orbit animation
        if (this.orbitAnimationFrameId !== null) {
            cancelAnimationFrame(this.orbitAnimationFrameId);
            this.orbitAnimationFrameId = null;
        }
        
        // Clear any pending click delay timeout
        if (this.clickDelayTimeout) {
            clearTimeout(this.clickDelayTimeout);
            this.clickDelayTimeout = null;
        }
        
        // Clear rotation detection timeout
        if (this.rotationDetectionTimeout) {
            clearTimeout(this.rotationDetectionTimeout);
            this.rotationDetectionTimeout = null;
        }
        
        // Clear key states
        Object.keys(this.keysPressed).forEach(key => {
            this.keysPressed[key] = false;
        });
        
        // Reset mouse states
        this.isMiddleMouseDown = false;
        this.isMiddleMouseRotating = false;
        
        // Remove external event listeners
        if (typeof window !== 'undefined') {
            this.externalEventListeners.forEach((listener, eventName) => {
                window.removeEventListener(eventName, listener);
            });
            this.externalEventListeners.clear();
            // console.log('Camera external event listeners removed');
        }
        
        // Remove pinch zoom wheel handler
        if (this.canvas && this.wheelHandler) {
            this.canvas.removeEventListener('wheel', this.wheelHandler);
            this.wheelHandler = null;
        }
        this.canvas = null;

        if (this.sphere) {
            this.sphere.dispose();
        }
        if (this.sphereHighlightLayer) {
            this.sphereHighlightLayer.dispose();
        }

        super.dispose();
        // console.log("adom camera dispose end");
    }

    /**
     * Set bounds for the camera target
     * Pass null for any bound you don't want to constrain
     */
    public setTargetBounds(
        minX: number | null = null,
        maxX: number | null = null,
        minY: number | null = null,
        maxY: number | null = null,
        minZ: number | null = null,
        maxZ: number | null = null
    ): void {
        this.targetBounds = { minX, maxX, minY, maxY, minZ, maxZ };
        this.enforceBounds = true;

        // Immediately enforce bounds if camera is already outside them
        this.checkAndEnforceBounds();
    }

    /**
     * Disable bounds checking for the camera target
     */
    public disableBounds(): void {
        this.enforceBounds = false;
    }

    /**
     * Check if the camera target is within bounds and correct if needed
     */
    private checkAndEnforceBounds(): void {
        let needsUpdate = false;
        let newTarget = this.target.clone();

        // Check X bounds
        if (this.targetBounds.minX !== null && newTarget.x < this.targetBounds.minX) {
            newTarget.x = this.targetBounds.minX;
            needsUpdate = true;
        }
        if (this.targetBounds.maxX !== null && newTarget.x > this.targetBounds.maxX) {
            newTarget.x = this.targetBounds.maxX;
            needsUpdate = true;
        }

        // Check Y bounds
        if (this.targetBounds.minY !== null && newTarget.y < this.targetBounds.minY) {
            newTarget.y = this.targetBounds.minY;
            needsUpdate = true;
        }
        if (this.targetBounds.maxY !== null && newTarget.y > this.targetBounds.maxY) {
            newTarget.y = this.targetBounds.maxY;
            needsUpdate = true;
        }

        // Check Z bounds
        if (this.targetBounds.minZ !== null && newTarget.z < this.targetBounds.minZ) {
            newTarget.z = this.targetBounds.minZ;
            needsUpdate = true;
        }
        if (this.targetBounds.maxZ !== null && newTarget.z > this.targetBounds.maxZ) {
            newTarget.z = this.targetBounds.maxZ;
            needsUpdate = true;
        }

        // Update the target if it was out of bounds
        if (true) {
            // Hide the sphere first
            const wasSphereVisible = this.sphere && this.sphere.isVisible;
            if (wasSphereVisible) {
                this.sphere.isVisible = false;
            }

            // Update the camera target
            this.setTarget(newTarget);

            // Instantly update the sphere position and make it visible again if it was visible
            if (wasSphereVisible) {
                this.sphere.position = newTarget.clone();
                this.sphere.isVisible = true;
            }
        }
    }

    /**
     * Set whether left-click alone should set rotation point (true) or require shift+alt+click (false)
     * @param useClickMode If true, normal left-clicks set rotation point like shift+alt+click
     * @param delayDuration Optional delay in milliseconds before triggering (defaults to 500ms/0.5sec)
     */
    public setClickToSetRotationPoint(useClickMode: boolean, delayDuration: number = 500): void {
        this.useClickToSetRotationPoint = useClickMode;
        this.clickDelayDuration = delayDuration;
    }

    /**
     * Get current click mode setting
     * @returns True if normal left-clicks set rotation point, false if shift+alt+click is required
     */
    public getClickToSetRotationPoint(): boolean {
        return this.useClickToSetRotationPoint;
    }

    /**
     * Process keyboard input for first-person camera movement
     */
    private processKeyboardInput(): void {
        if (!Object.values(this.keysPressed).some(pressed => pressed)) {
            return; // No keys are pressed, skip processing
        }
        
        // Get camera direction vectors for movement
        // IMPORTANT: For Babylon.js, Vector3.Forward() is actually pointing toward negative Z
        // So we need to reverse it to get the proper forward direction for first-person movement
        const forward = this.getDirection(new Vector3(0, 0, -1)); // True forward in camera space
        const right = this.getDirection(Vector3.Right());         // Right in camera space
        const up = this.upVector;                                // Global up vector
        
        // For true FPS-style movement, we want movement in the horizontal plane
        // So we zero out the Z component and renormalize
        const horizontalForward = new Vector3(forward.x, forward.y, 0).normalize();
        const horizontalRight = new Vector3(right.x, right.y, 0).normalize();
        
        // Create move direction vector based on keys pressed
        const moveDirection = new Vector3(0, 0, 0);
        
        // Correct W/S logic: 
        // W should move forward along horizontalForward
        // S should move backward (opposite of horizontalForward)
        if (this.keysPressed['w']) {
            // W = move forward in the direction we're facing
            moveDirection.addInPlace(horizontalForward);
        }
        if (this.keysPressed['s']) {
            // S = move backward, opposite of the direction we're facing
            moveDirection.addInPlace(horizontalForward.scale(-1));
        }
        if (this.keysPressed['a']) {
            // A = strafe left (opposite of right)
            moveDirection.addInPlace(horizontalRight.scale(-1));
        }
        if (this.keysPressed['d']) {
            // D = strafe right
            moveDirection.addInPlace(horizontalRight);
        }
        if (this.keysPressed['q']) {
            // Q = move down
            moveDirection.addInPlace(up.scale(-1));
        }
        if (this.keysPressed['e']) {
            // E = move up
            moveDirection.addInPlace(up);
        }
        
        // Normalize the move direction if we have any movement
        if (moveDirection.length() > 0) {
            moveDirection.normalize();
            
            // Scale by move speed, proportional to camera radius so tiny/huge models feel the same
            const scaledSpeed = this.moveSpeed * (this.radius / 100);
            const movement = moveDirection.scale(scaledSpeed);
            
            // Move both camera position and target to maintain orientation
            this.position.addInPlace(movement);
            this.target.addInPlace(movement);
            
            // Update orbit sphere position if visible
            if (this.sphere && this.sphere.isVisible) {
                this.sphere.position = this.target.clone();
            }
        }
    }
    
    /**
     * Set the movement speed for first-person controls
     */
    public setMoveSpeed(speed: number): void {
        this.baseMoveSpeed = speed;
        // Apply precision mode slowdown if active
        this.moveSpeed = this.precisionMode ? speed / 4 : speed;
    }
    
    /**
     * Get the current movement speed
     */
    public getMoveSpeed(): number {
        return this.moveSpeed;
    }
    
    /**
     * Rotate camera to a specific viewcube angle
     * @param alpha The target alpha angle
     * @param beta The target beta angle
     * @param duration Animation duration in seconds (defaults to 0.5)
     */
    public rotateToCubeAngle(alpha: number, beta: number, duration: number = 0.5): void {
        // Calculate shortest path for alpha rotation
        const normalizeAngle = (angle: number): number => {
            return angle - Math.PI * 2 * Math.floor((angle + Math.PI) / (2 * Math.PI));
        };
        
        const shortestAngleDistance = (from: number, to: number): number => {
            const normalizedFrom = normalizeAngle(from);
            const normalizedTo = normalizeAngle(to);
            const difference = normalizedTo - normalizedFrom;
            if (difference > Math.PI) {
                return difference - Math.PI * 2;
            } else if (difference < -Math.PI) {
                return difference + Math.PI * 2;
            }
            return difference;
        };
        
        const targetAlpha = this.alpha + shortestAngleDistance(this.alpha, alpha);
        const targetBeta = this.beta + shortestAngleDistance(this.beta, beta);

        if (duration <= 0) {
            this.alpha = targetAlpha;
            this.beta = targetBeta;
            return;
        }

        const fps = 60;
        const totalFrames = fps * duration;
        
        // Create alpha animation
        const alphaAnimation = new Animation(
            'alphaAnimation',
            'alpha',
            fps,
            Animation.ANIMATIONTYPE_FLOAT,
            Animation.ANIMATIONLOOPMODE_CONSTANT
        );
        alphaAnimation.setKeys([
            { frame: 0, value: this.alpha },
            { frame: totalFrames, value: targetAlpha }
        ]);
        
        // Create beta animation
        const betaAnimation = new Animation(
            'betaAnimation',
            'beta',
            fps,
            Animation.ANIMATIONTYPE_FLOAT,
            Animation.ANIMATIONLOOPMODE_CONSTANT
        );
        betaAnimation.setKeys([
            { frame: 0, value: this.beta },
            { frame: totalFrames, value: targetBeta }
        ]);
        
        this.animations = [alphaAnimation, betaAnimation];
        this._scene.beginAnimation(this, 0, totalFrames, false);
    }
    
    /**
     * Rotate camera to top view
     */
    public rotateToCubeTop(duration: number = 0.5): void {
        // Top face: position [0, 0, cr] where cr = 2
        // For top view: looking straight down
        const alpha = this.alpha; // Keep current alpha (rotation around Z)
        const beta = 0; // Looking straight down from top
        this.rotateToCubeAngle(alpha, beta, duration);
    }
    
    /**
     * Rotate camera to top-front-right corner view
     */
    public rotateToCubeTopFrontRight(duration: number = 0.5): void {
        // Top-front-right corner: position [cr-es/2, -(cr-es/2), cr-es/2]
        // With cr=2, es=0.75: position ≈ [1.625, -1.625, 1.625]
        const x = 1.625, y = -1.625, z = 1.625;
        const adjust = -Math.PI / 2;
        const alpha = Math.atan2(x, y) + adjust;
        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));
        this.rotateToCubeAngle(alpha, beta, duration);
    }
    
    /**
     * Rotate camera to top-front-left corner view
     */
    public rotateToCubeTopFrontLeft(duration: number = 0.5): void {
        // Top-front-left corner: position [-(cr-es/2), -(cr-es/2), cr-es/2]
        // With cr=2, es=0.75: position ≈ [-1.625, -1.625, 1.625]
        const x = -1.625, y = -1.625, z = 1.625;
        const adjust = -Math.PI / 2;
        const alpha = Math.atan2(x, y) + adjust;
        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));
        this.rotateToCubeAngle(alpha, beta, duration);
    }
    
    /**
     * Rotate camera to top-back-right corner view
     */
    public rotateToCubeTopBackRight(duration: number = 0.5): void {
        // Top-back-right corner: position [cr-es/2, cr-es/2, cr-es/2]
        // With cr=2, es=0.75: position ≈ [1.625, 1.625, 1.625]
        const x = 1.625, y = 1.625, z = 1.625;
        const adjust = -Math.PI / 2;
        const alpha = Math.atan2(x, y) + adjust;
        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));
        this.rotateToCubeAngle(alpha, beta, duration);
    }
    
    /**
     * Rotate camera to top-back-left corner view
     */
    public rotateToCubeTopBackLeft(duration: number = 0.5): void {
        // Top-back-left corner: position [-(cr-es/2), cr-es/2, cr-es/2]
        // With cr=2, es=0.75: position ≈ [-1.625, 1.625, 1.625]
        const x = -1.625, y = 1.625, z = 1.625;
        const adjust = -Math.PI / 2;
        const alpha = Math.atan2(x, y) + adjust;
        const beta = Math.acos(z / Math.sqrt(x * x + y * y + z * z));
        this.rotateToCubeAngle(alpha, beta, duration);
    }
    
    /**
     * Toggle between orthographic and perspective camera modes.
     * When linked to the moving camera, switching to orthographic is blocked.
     * @returns The new camera mode (ORTHOGRAPHIC_CAMERA or PERSPECTIVE_CAMERA)
     */
    public toggleCameraMode(): number {
        if (this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA) {
            this.mode = ArcRotateCamera.PERSPECTIVE_CAMERA;
            // console.log('Switched to Perspective Camera');
        } else {
            this.mode = ArcRotateCamera.ORTHOGRAPHIC_CAMERA;
            // Update orthographic bounds when switching to ortho mode
            this.updateOrthographicBounds();
            // console.log('Switched to Orthographic Camera');
        }
        return this.mode;
    }
    
    /**
     * Get the current camera mode
     * @returns The current camera mode (ORTHOGRAPHIC_CAMERA or PERSPECTIVE_CAMERA)
     */
    public getCameraMode(): number {
        return this.mode;
    }
    
    /**
     * Check if camera is in orthographic mode
     * @returns True if orthographic, false if perspective
     */
    public isOrthographic(): boolean {
        return this.mode === ArcRotateCamera.ORTHOGRAPHIC_CAMERA;
    }
}