UNPKG

animouse

Version:

lightweight animation state machine for three js

2,184 lines 104 kB
import { MathUtils, LoopOnce, Vector2 } from 'three';

/**
 * Defines events related to a blend state and its internal AnimationActions.
 *
 * - ENTER and EXIT describe when the blend state itself becomes active or inactive in the state machine.
 * - PLAY, STOP, ITERATE, and FINISH describe the lifecycle of individual AnimationActions inside this blend state.
 */
var AnimationStateEvent;
(function (AnimationStateEvent) {
    /**
     * Fired when the blend state itself becomes active.
     * Indicates that a transition into this state has started.
     */
    AnimationStateEvent["ENTER"] = "enter";
    /**
     * Fired when the blend state itself becomes inactive.
     * Indicates that a transition away from this state has started.
     */
    AnimationStateEvent["EXIT"] = "exit";
    /**
     * Fired when an individual AnimationAction inside this blend state starts playing.
     * For example, when a walk or run action becomes active due to weights.
     */
    AnimationStateEvent["PLAY"] = "play";
    /**
     * Fired when an individual AnimationAction inside this blend state stops playing.
     * For example, when a walk or run action stops as its weights reach zero.
     */
    AnimationStateEvent["STOP"] = "stop";
    /**
     * Fired when an individual AnimationAction inside this blend state completes one full loop.
     * Fires every cycle for looping actions (LoopRepeat, LoopPingPong).
     */
    AnimationStateEvent["ITERATE"] = "iterate";
    /**
     * Fired when an individual AnimationAction inside this blend state reaches its natural end.
     * This applies to non-looping actions (LoopOnce) that finish playing completely.
     */
    AnimationStateEvent["FINISH"] = "finish";
})(AnimationStateEvent || (AnimationStateEvent = {}));

/**
 * Small epsilon value used for floating-point comparisons.
 */
const EPSILON = 1e-6;
/**
 * Two times PI (2π), commonly used for full circle calculations.
 */
const PI2 = Math.PI * 2;
let lastAnchorIndex = 0;
/**
 * Gets the next unique anchor index for animation anchors.
 * Each call returns an incrementing integer starting from 0.
 * @returns The next available anchor index
 */
function getNextAnchorIndex() {
    return lastAnchorIndex++;
}

/**
 * Asserts that a number is valid (finite and does not exceed safe integer range).
 *
 * @param value - The number to validate
 * @param message - Custom error message for validation failure
 * @throws {Error} When the value is not finite or exceeds MAX_SAFE_INTEGER
 */
function assertValidNumber(value, message) {
    if (!Number.isFinite(value)) {
        throw new Error(`${message}: value must be a finite number`);
    }
    if (Math.abs(value) > Number.MAX_SAFE_INTEGER) {
        throw new Error(`${message}: value exceeds maximum safe integer range`);
    }
}
/**
 * Asserts that an azimuth angle is valid (finite, within safe range, and between 0 and 2π).
 *
 * @param value - The azimuth angle in radians to validate
 * @param message - Custom error message for validation failure
 * @throws {Error} When the value is not finite, exceeds MAX_SAFE_INTEGER, or is not between 0 and 2π radians
 * @see {@link assertValidNumber} for base number validation
 */
function assertValidAzimuth(value, message) {
    assertValidNumber(value, message);
    if (value < 0 || value > PI2) {
        throw new Error(`${message}: azimuth must be between 0 and 2π radians`);
    }
}
/**
 * Asserts that a number is within the unit range [0, 1].
 *
 * @param value - The number to validate
 * @param message - Custom error message for validation failure
 * @throws {Error} When the value is not within the range [0, 1]
 * @see {@link assertValidNumber} for base number validation
 */
function assertValidUnitRange(value, message) {
    assertValidNumber(value, message);
    if (value < 0 || value > 1) {
        throw new Error(`${message}: value must be between 0 and 1`);
    }
}
/**
 * Asserts that a number is positive (greater than or equal to EPSILON).
 * Uses EPSILON to account for floating-point precision errors.
 *
 * @param value - The number to validate
 * @param message - Custom error message for validation failure
 * @throws {Error} When the value is less than EPSILON
 * @see {@link assertValidNumber} for base number validation
 */
function assertValidPositiveNumber(value, message) {
    assertValidNumber(value, message);
    if (value < EPSILON) {
        throw new Error(`${message}: value must be greater than or equal to ${EPSILON}`);
    }
}
/**
 * Asserts that a number is non-negative (greater than or equal to 0).
 *
 * @param value - The number to validate
 * @param message - Custom error message for validation failure
 * @throws {Error} When the value is negative
 * @see {@link assertValidNumber} for base number validation
 */
function assertValidNonNegativeNumber(value, message) {
    assertValidNumber(value, message);
    if (value < 0) {
        throw new Error(`${message}: value must be greater than or equal to 0`);
    }
}

/**
 * Animation state machine for managing complex state transitions and blending.
 *
 * Provides a comprehensive system for controlling animation states with support for:
 * - **Event-based transitions**: Triggered by specific events with optional conditions
 * - **Automatic transitions**: Triggered when animations complete (ITERATE/FINISH events)
 * - **Data-driven transitions**: Evaluated continuously based on condition functions
 *
 * The state machine handles smooth blending between states using configurable transition
 * durations, automatically managing animation weights and ensuring proper lifecycle events.
 * Multiple states can be active simultaneously during transitions, with weights smoothly
 * interpolated using linear interpolation.
 */
class AnimationMachine {
    /**
     * Creates a new animation state machine with the specified initial state.
     * Immediately activates the initial state with full influence and enters it.
     *
     * @param initialState - The starting animation state to activate
     * @param mixer - The THREE.js animation mixer for updating animations
     */
    constructor(initialState, mixer) {
        /** States that are currently fading out during transitions */
        this._private_fadingStates = [];
        /** Map of event names to their possible transitions */
        this._private_eventTransitions = new Map();
        /** Map of states to their automatic transitions that occur at animation end */
        this._private_automaticTransitions = new Map();
        /** Map of states to their data-driven transitions */
        this._private_dataTransitions = new Map();
        this._private_currentStateInternal = initialState;
        this._private_currentStateInternal["onEnterInternal"]();
        this._private_currentStateInternal["setInfluenceInternal"](1);
        this._private_mixer = mixer;
        this._private_eventTransitions = new Map();
    }
    /**
     * Gets the currently active animation state.
     * During transitions, this returns the state being transitioned to.
     *
     * @returns The current active animation state
     */
    get currentState() {
        return this._private_currentStateInternal;
    }
    /**
     * Adds a new event-triggered transition to the state machine.
     * Multiple transitions can be registered for the same event with different source states.
     * When an event is handled, the first matching transition will be executed.
     *
     * @param event - The event name or identifier that triggers this transition
     * @param transition - The transition configuration with target state and duration
     * @throws {Error} When transition duration is not a finite non-negative number
     * @throws {Error} When transition creates a recursive loop (from === to)
     * @throws {Error} When transition already exists for the same event and source state
     * @see {@link assertValidNonNegativeNumber} for duration validation details
     */
    addEventTransition(event, transition) {
        assertValidNonNegativeNumber(transition.duration, "Event transition duration");
        if (transition.from === transition.to) {
            throw new Error("Event animation transition can't create a recursive loop to itself");
        }
        const transitions = this._private_eventTransitions.get(event) ?? [];
        if (!transition.condition) {
            for (const someTransition of transitions) {
                if (!someTransition.condition &&
                    someTransition.from === transition.from) {
                    throw new Error("Event animation transition already exists");
                }
            }
        }
        transitions.push(transition);
        this._private_eventTransitions.set(event, transitions);
    }
    /**
     * Adds an automatic transition that occurs when an animation completes.
     * Listens for ITERATE and FINISH events from the source state to trigger transitions.
     * Only one automatic transition can be registered per source state.
     *
     * @param from - The source state that will trigger the automatic transition
     * @param transition - The transition configuration with target state and duration
     * @throws {Error} When transition duration is not a finite non-negative number
     * @throws {Error} When transition creates a recursive loop (from === to)
     * @throws {Error} When an automatic transition already exists for the source state
     * @see {@link assertValidNonNegativeNumber} for duration validation details
     * @see {@link AnimationStateEvent.ITERATE} for iteration event details
     * @see {@link AnimationStateEvent.FINISH} for completion event details
     */
    addAutomaticTransition(from, transition) {
        assertValidNonNegativeNumber(transition.duration, "Automatic transition duration");
        if (from === transition.to) {
            throw new Error("Automatic animation transition can't create a recursive loop to itself");
        }
        if (this._private_automaticTransitions.has(from)) {
            throw new Error("Automatic transition already exists");
        }
        this._private_automaticTransitions.set(from, transition);
        from.on(AnimationStateEvent.ITERATE, this._private_onStateIteration, this);
        from.on(AnimationStateEvent.FINISH, this._private_onStateIteration, this);
    }
    /**
     * Adds a data-driven transition that is evaluated continuously during updates.
     * The condition function is called each frame when the source state is active.
     * Multiple data transitions can be registered per state, but only one per target.
     *
     * @param from - The source state where the transition can originate
     * @param transition - The transition configuration with condition and target state
     * @throws {Error} When transition duration is not a finite non-negative number
     * @throws {Error} When transition creates a recursive loop (from === to)
     * @throws {Error} When a data transition to the same target already exists
     * @see {@link assertValidNonNegativeNumber} for duration validation details
     */
    addDataTransition(from, transition) {
        assertValidNonNegativeNumber(transition.duration, "Data transition duration");
        if (from === transition.to) {
            throw new Error("Automatic animation transition can't create a recursive loop to itself");
        }
        const transitions = this._private_dataTransitions.get(from) ?? [];
        transitions.push(transition);
        this._private_dataTransitions.set(from, transitions);
    }
    /**
     * Handles an event by checking and executing the first matching transition.
     * Evaluates all registered transitions for the event in registration order,
     * executing the first one that matches the current state and passes conditions.
     *
     * @param event - The event name or identifier to handle
     * @param args - Additional arguments passed to transition condition functions
     * @returns True if a transition was executed, false if no matching transition found
     */
    handleEvent(event, ...args) {
        const transitions = this._private_eventTransitions.get(event);
        if (transitions === undefined) {
            return false;
        }
        for (const { from, to, duration, condition } of transitions) {
            const isValidFromState = !from || from === this._private_currentStateInternal;
            const isValidCondition = !condition || condition(from, to, event, ...args);
            if (isValidFromState && isValidCondition) {
                return this._private_transitionTo(to, duration);
            }
        }
        return false;
    }
    /**
     * Updates the animation state machine and all animations for one frame.
     *
     * The update process:
     * 1. Updates current state and all fading states with frame timing
     * 2. Evaluates data-driven transitions for potential state changes
     * 3. Progresses active transitions by interpolating state influences
     * 4. Cleans up completed transitions and resets influences
     * 5. Updates the THREE.js animation mixer
     *
     * @param deltaTime - Time elapsed since last update in seconds (finite non-negative number)
     * @throws {Error} When deltaTime is not a finite non-negative number
     * @see {@link assertValidNonNegativeNumber} for deltaTime validation details
     */
    update(deltaTime) {
        assertValidNonNegativeNumber(deltaTime, "Delta time");
        // Update current state
        if (this._private_currentStateInternal.influence > 0) {
            this._private_currentStateInternal["onTickInternal"](deltaTime);
        }
        // Update all fading states
        for (const state of this._private_fadingStates) {
            if (state.influence > 0) {
                state["onTickInternal"](deltaTime);
            }
        }
        const transition = this._private_dataTransitions.get(this._private_currentStateInternal)
            ?.find((transition) => transition.condition(this._private_currentStateInternal, transition.to, ...(transition.data ?? [])));
        if (transition) {
            this._private_transitionTo(transition.to, transition.duration);
        }
        if (this._private_transitionElapsedTime !== undefined) {
            const t = this._private_transitionElapsedTime === 0
                ? 1
                : Math.min(1, deltaTime / this._private_transitionElapsedTime);
            for (const state of this._private_fadingStates) {
                state["setInfluenceInternal"](MathUtils.lerp(state.influence, 0, t));
            }
            this._private_currentStateInternal["setInfluenceInternal"](MathUtils.lerp(this._private_currentStateInternal.influence, 1, t));
            this._private_transitionElapsedTime = Math.max(0, this._private_transitionElapsedTime - deltaTime);
            if (this._private_transitionElapsedTime === 0) {
                for (const state of this._private_fadingStates) {
                    state["setInfluenceInternal"](0);
                }
                this._private_fadingStates = [];
                this._private_currentStateInternal["setInfluenceInternal"](1);
                this._private_transitionElapsedTime = undefined;
            }
        }
        this._private_mixer.update(deltaTime);
    }
    /**
     * Transitions to a new animation state over the specified duration.
     *
     * @private
     * @param {AnimationState} state - The state to transition to
     * @param {number} duration - The duration of the transition in seconds
     */
    _private_transitionTo(state, duration) {
        if (this._private_currentStateInternal === state) {
            return false;
        }
        this._private_fadingStates = this._private_fadingStates.filter((s) => s !== state);
        this._private_fadingStates.push(this._private_currentStateInternal);
        this._private_currentStateInternal["onExitInternal"]();
        this._private_currentStateInternal = state;
        this._private_currentStateInternal["onEnterInternal"]();
        this._private_transitionElapsedTime = duration;
        return true;
    }
    /**
     * Handles animation iteration events for automatic transitions.
     *
     * @private
     * @param {AnimationState} state - The state that completed an iteration
     */
    _private_onStateIteration(action, state) {
        if (state === this._private_currentStateInternal) {
            const transition = this._private_automaticTransitions.get(this._private_currentStateInternal);
            if (transition) {
                this._private_transitionTo(transition.to, transition.duration);
            }
        }
    }
}

class t{constructor(t,i,r=0){this.t=new WeakMap,this.i=new Map,void 0!==t&&(void 0===i?this.i.set(t,r):this.t.set(i,new Map([[t,r]])));}insert(t,i,r=0){if(void 0===i){if(this.i.has(t))return;return void this.i.set(t,r)}const s=this.t.get(i);void 0===s?this.t.set(i,new Map([[t,r]])):s.has(t)||s.set(t,r);}remove(t,i){if(void 0===i)return void this.i.delete(t);const r=this.t.get(i);void 0!==r&&(r.delete(t),0===r.size&&this.t.delete(i));}has(t,i){if(void 0===i)return this.i.has(t);const r=this.t.get(i);return void 0!==r&&r.has(t)}getPriority(t,i){if(void 0===i)return this.i.get(t);const r=this.t.get(i);return void 0!==r?r.get(t):void 0}}class i{constructor(){this.o=new Map;}on(t,i,r,s=0){return this.h(t,false,s,i,r),this}once(t,i,r,s=0){return this.h(t,true,s,i,r),this}off(t,i,r){const s=this.o.get(t);if(void 0===s)return this;s.u&&(s.u=false,s.v=s.v.slice());const o=s.v;if(0===o.length)return this.o.delete(t),this;if(void 0===i){this.o.delete(t);for(let t=0;t<o.length;t++){const i=o[t];s.l.remove(i._,i.p);}return this}{const e=o.length;if(o[0]._===i&&o[0].p===r)return 1===e?this.o.delete(t):o.shift(),this;const n=e-1;if(o[n]._===i&&o[n].p===r)return o.pop(),this;if(e<10||o[0].M===o[n].M)for(let t=1;t<n;t++){const s=o[t];if(s._===i&&s.p===r)return o.splice(t,1),this}else {const t=s.l.getPriority(i,r);if(void 0===t)return this;let e=0,n=o.length-1;for(;e<=n;){const i=e+n>>>1;o[i].M<t?e=i+1:n=i-1;}const c=e;for(n=o.length-1;e<=n;){const i=e+n>>>1;o[i].M<=t?e=i+1:n=i-1;}const h=n;for(let t=c;t<=h;t++)if(o[t]._===i&&o[t].p===r)return o.splice(t,1),this}}return this}emit(t,...i){const r=this.o.get(t);if(void 0===r)return  false;if(0===r.v.length)return  false;r.u?r.v=r.v.slice():r.u=true;const s=r.v;let o=false;for(let t=0;t<s.length;t++){const r=s[t];r._.apply(r.p,i),r.k&&(o=true,r.m=true);}const e=this.o.get(t);if(void 0===e)return  true;e.v===s&&(e.u=false);const n=e.v;if(o){let i=0;for(let t=0;t<n.length;t++){const r=n[t];r.m?e.l.remove(r._,r.p):n[i++]=r;}0===i?this.o.delete(t):n.length=i;}return  true}h(i,r,s,o,e){let n=this.o.get(i);if(void 0===n)return void this.o.set(i,{v:[{_:o,p:e,M:s,k:r,m:false}],u:false,l:new t(o,e,s)});if(n.l.has(o,e))throw Error("Event listener already exists");n.l.insert(o,e,s);const c={_:o,p:e,M:s,k:r,m:false};n.u&&(n.u=false,n.v=n.v.slice());{const t=n.v,i=c.M;if(0===t.length||t[t.length-1].M<=i)return void t.push(c);if(t[0].M>=i)return void t.unshift(c);{let r=0,s=t.length;for(;r<s;){const o=r+s>>>1;t[o].M<i?r=o+1:s=o;}t.splice(r,0,c);}}}}

/**
 * Abstract base class for animation states in the animation state machine.
 * Manages state influence (weight) and provides lifecycle event handling.
 *
 * This class is designed to be extended by concrete animation states and
 * controlled by the animation state machine.
 */
class AnimationState extends i {
    constructor() {
        super(...arguments);
        /**
         * The name identifier for this animation state.
         * Used to identify and reference the state within the animation state machine.
         */
        this.name = "";
        /**
         * Internal storage for the state's influence value.
         * Represents the weight/contribution of this state in the animation state machine.
         */
        this.influenceInternal = 0;
        /**
         * Map storing time-based event callbacks for each anchor.
         * Maps anchors to their respective time events, where each time event
         * is identified by a normalized time value and associated event name.
         */
        this.timeEvents = new Map();
    }
    /**
     * Gets the current influence (weight) of this animation state.
     * The influence determines how much this state contributes to the overall animation.
     *
     * @returns The current influence value, always in range [0, 1]
     */
    get influence() {
        return this.influenceInternal;
    }
    /**
     * Internal method called by the animation state machine when entering this state.
     * Emits the ENTER event with this state instance as data.
     *
     * @internal This method is intended to be called only by the animation state machine
     */
    ["onEnterInternal"]() {
        this.emit(AnimationStateEvent.ENTER, this);
    }
    /**
     * Internal method called by the animation state machine when exiting this state.
     * Emits the EXIT event with this state instance as data.
     *
     * @internal This method is intended to be called only by the animation state machine
     */
    ["onExitInternal"]() {
        this.emit(AnimationStateEvent.EXIT, this);
    }
    /**
     * Internal method to register a time-based event callback for an anchor.
     * Normalizes the unit time to avoid floating-point precision issues and
     * creates a unique event identifier for the time/anchor combination.
     *
     * @param anchor - The animation anchor to associate the time event with
     * @param unitTime - Time in unit range [0, 1] when the event should fire
     * @param callback - Function to call when the time event occurs
     * @param isOnce - Whether the event should fire only once or repeatedly
     * @internal This method is intended to be called only by concrete animation state implementations
     */
    onTimeEventInternal(anchor, unitTime, callback, isOnce) {
        assertValidUnitRange(unitTime, "Unit time");
        const roundedTime = Math.round(unitTime * 100) / 100;
        const events = this.timeEvents.get(anchor) ?? new Map();
        this.timeEvents.set(anchor, events);
        const event = events.get(roundedTime) ?? `${roundedTime}_${anchor.index}`;
        events.set(roundedTime, event);
        isOnce ? this.once(event, callback) : this.on(event, callback);
    }
    /**
     * Internal method to unregister a time-based event callback for an anchor.
     * Removes the callback from the specified time point and cleans up empty
     * time event maps to prevent memory leaks.
     *
     * @param anchor - The animation anchor to remove the time event from
     * @param unitTime - Time in unit range [0, 1] where the event was registered
     * @param callback - The callback function to remove
     * @internal This method is intended to be called only by concrete animation state implementations
     */
    offTimeEventInternal(anchor, unitTime, callback) {
        assertValidUnitRange(unitTime, "Unit time");
        const roundedTime = Math.round(unitTime * 100) / 100;
        const events = this.timeEvents.get(anchor);
        if (!events) {
            return;
        }
        const event = events.get(roundedTime);
        if (!event) {
            return;
        }
        this.off(event, callback);
        events.delete(roundedTime);
        if (events.size === 0) {
            this.timeEvents.delete(anchor);
        }
    }
    /**
     * Updates the anchor's time tracking and emits iteration events when appropriate.
     * Handles event firing logic based on animation time progression and anchor duration.
     * Tracks whether iteration events have been fired to prevent duplicate emissions.
     *
     * @param anchor - The anchor object containing action, duration, and event tracking state
     * @param deltaTime - Time elapsed since the last frame, in milliseconds
     * @throws {Error} When the anchor's action is not running
     * @internal This method is intended to be called only by concrete animation state implementations
     */
    updateAnchorTime(anchor, deltaTime) {
        const action = anchor.action;
        if (anchor.action.weight === 0) {
            throw new Error(`Cannot update anchor time for a non-running action: ${this.name}`);
        }
        const time = action.time;
        const duration = anchor.duration;
        if (time < duration && time + deltaTime >= duration) {
            this.emit(anchor.iterationEventType, action, this);
        }
    }
    resetFinishedAction(anchor) {
        const action = anchor.action;
        if (action.loop === LoopOnce && action.time === anchor.duration) {
            action.time = 0;
            action.paused = false;
            action.enabled = true;
        }
    }
    /**
     * Processes and fires time-based events for an anchor during animation playback.
     * Checks if the animation has crossed any registered time event thresholds
     * and emits the corresponding events with the action and state as parameters.
     *
     * @param anchor - The animation anchor to process time events for
     * @param deltaTime - Time elapsed since the last frame, in milliseconds
     * @internal This method is intended to be called only by concrete animation state implementations
     */
    processTimeEvents(anchor, deltaTime) {
        const events = this.timeEvents.get(anchor);
        if (events) {
            const action = anchor.action;
            const invDeltaTime = deltaTime * anchor.invDuration;
            for (const [eventTime, eventName] of events) {
                const actionTime = action.time * anchor.invDuration;
                if (actionTime < eventTime && actionTime + invDeltaTime >= eventTime) {
                    this.emit(eventName, action, this);
                }
            }
        }
    }
}

/**
 * Animation state that wraps a single Three.js AnimationAction.
 * Manages the lifecycle of a single animation clip, handling playback,
 * weight control, and iteration events.
 *
 * This state automatically detects animation completion and restart events,
 * emitting appropriate state events based on the animation's loop type.
 */
class ClipState extends AnimationState {
    /**
     * Creates a new ClipState from a Three.js AnimationAction.
     * Initializes the animation to a stopped state and configures iteration events
     * based on the animation's loop type. Sets up anchor with duration tracking
     * and event state management.
     *
     * @param animationAction - The Three.js AnimationAction to wrap (finite duration required)
     * @throws {Error} When the animation clip duration is not a positive finite number
     * @see {@link assertValidPositiveNumber} for duration validation details
     */
    constructor(animationAction) {
        super();
        const duration = animationAction.getClip().duration;
        assertValidPositiveNumber(duration, "Clip duration");
        animationAction.stop();
        animationAction.time = 0;
        animationAction.weight = 0;
        animationAction.paused = false;
        animationAction.enabled = false;
        this._private_anchor = {
            index: getNextAnchorIndex(),
            action: animationAction,
            get weight() {
                throw new Error("ClipState weight is not accessible");
            },
            duration,
            invDuration: 1 / duration,
            iterationEventType: animationAction.loop === LoopOnce
                ? AnimationStateEvent.FINISH
                : AnimationStateEvent.ITERATE,
        };
    }
    /**
     * Registers a callback to be called when the animation reaches a specific time.
     * The callback will be invoked every time the animation crosses the specified time threshold.
     *
     * @param unitTime - Time in unit range [0, 1] when the callback should be invoked
     * @param callback - Function to call when the time event occurs, receives the action and state as parameters
     */
    onTimeEvent(unitTime, callback) {
        this.onTimeEventInternal(this._private_anchor, unitTime, callback, false);
    }
    /**
     * Registers a callback to be called once when the animation reaches a specific time.
     * The callback will be invoked only the first time the animation crosses the specified time threshold.
     *
     * @param unitTime - Time in unit range [0, 1] when the callback should be invoked
     * @param callback - Function to call when the time event occurs, receives the action and state as parameters
     */
    onceTimeEvent(unitTime, callback) {
        this.onTimeEventInternal(this._private_anchor, unitTime, callback, true);
    }
    /**
     * Removes a previously registered time event callback.
     * Unregisters the callback from the specified time point and cleans up associated resources.
     *
     * @param unitTime - Time in unit range [0, 1] where the callback was registered
     * @param callback - The callback function to remove
     */
    offTimeEvent(unitTime, callback) {
        this.offTimeEventInternal(this._private_anchor, unitTime, callback);
    }
    /**
     * Internal method to set the influence of this animation state.
     * Controls animation playback: starts animation when influence becomes positive,
     * stops when influence becomes zero, and adjusts weight for intermediate values.
     * Resets animation time and event tracking state on playback transitions.
     *
     * @param influence - The new influence value in range [0, 1] (finite number)
     * @throws {Error} When influence is not a finite number or is outside the range [0, 1]
     * @internal This method is intended to be called only by the animation state machine
     * @see {@link AnimationStateEvent.PLAY} for play event details
     * @see {@link AnimationStateEvent.STOP} for stop event details
     */
    ["setInfluenceInternal"](influence) {
        assertValidUnitRange(influence, "Influence");
        if (this.influenceInternal === influence) {
            return;
        }
        this.influenceInternal = influence;
        const anchor = this._private_anchor;
        const animationAction = anchor.action;
        if (influence > 0 && animationAction.weight === 0) {
            animationAction.enabled = true;
            animationAction.paused = false;
            animationAction.time = 0;
            animationAction.play();
            this.emit(AnimationStateEvent.PLAY, animationAction, this);
        }
        else if (influence === 0 && animationAction.weight > 0) {
            animationAction.stop();
            animationAction.time = 0;
            animationAction.paused = false;
            animationAction.enabled = false;
            this.emit(AnimationStateEvent.STOP, animationAction, this);
        }
        animationAction.weight = influence;
    }
    /**
     * Internal method called on each frame to update animation state.
     * Delegates to the inherited updateAnchorTime method to handle time tracking
     * and iteration event emission based on animation progress.
     *
     * @internal This method is intended to be called only by the animation state machine
     * @see {@link updateAnchorTime} for time tracking and event emission details
     */
    ["onTickInternal"](deltaTime) {
        if (this.influence === 0) {
            throw new Error(`${this.name}: cannot update anchor time because the animation influence is zero`);
        }
        this.processTimeEvents(this._private_anchor, deltaTime);
        this.updateAnchorTime(this._private_anchor, deltaTime);
    }
    ["onEnterInternal"]() {
        super.onEnterInternal();
        if (this.influence > 0) {
            this.resetFinishedAction(this._private_anchor);
        }
    }
}

/** The number of vertices in a triangle. */
const TRIANGLE_VERTEX_COUNT = 3;
/**
 * Precomputes triangle data for efficient subsequent operations.
 * Calculates circumcenter, bounding box, and barycentric coordinate helpers.
 *
 * @param a - First vertex of the triangle (Vector2Like coordinates)
 * @param b - Second vertex of the triangle (Vector2Like coordinates)
 * @param c - Third vertex of the triangle (Vector2Like coordinates)
 * @returns Precomputed triangle data cache for efficient operations
 * @throws {Error} When triangle is degenerate or coordinates are invalid
 * @see {@link assertValidNumber} for coordinate validation details
 */
function precomputeTriangle(a, b, c) {
    assertValidNumber(a.x, "a.x");
    assertValidNumber(a.y, "a.y");
    assertValidNumber(b.x, "b.x");
    assertValidNumber(b.y, "b.y");
    assertValidNumber(c.x, "c.x");
    assertValidNumber(c.y, "c.y");
    const u = { x: b.x - a.x, y: b.y - a.y };
    const v = { x: c.x - a.x, y: c.y - a.y };
    const d00 = u.x * u.x + u.y * u.y;
    const d01 = u.x * v.x + u.y * v.y;
    const d11 = v.x * v.x + v.y * v.y;
    const bDet = d00 * d11 - d01 * d01;
    if (Math.abs(bDet) < EPSILON) {
        throw new Error(`Degenerate triangle detected: determinant is too close to zero (${bDet}). ` +
            `Triangle points are a(${a.x}, ${a.y}), b(${b.x}, ${b.y}), c(${c.x}, ${c.y}).`);
    }
    const cDet = 2 * (u.x * v.y - u.y * v.x);
    const circumcenter = {
        x: a.x + (v.y * d00 - u.y * d11) / cDet,
        y: a.y + (u.x * d11 - v.x * d00) / cDet,
    };
    return {
        origin: a,
        circumcenter,
        circumradiusSquared: (a.x - circumcenter.x) ** 2 + (a.y - circumcenter.y) ** 2,
        u,
        v,
        d00,
        d01,
        d11,
        invDenom: 1 / bDet,
        min: {
            x: Math.min(a.x, b.x, c.x),
            y: Math.min(a.y, b.y, c.y),
        },
        max: {
            x: Math.max(a.x, b.x, c.x),
            y: Math.max(a.y, b.y, c.y),
        },
    };
}
/**
 * Calculates barycentric coordinates for a point relative to a triangle.
 * Returns undefined if the point lies outside the triangle boundaries.
 *
 * @param point - The point to calculate barycentric coordinates for (Vector2Like coordinates)
 * @param cache - Precomputed triangle data containing vectors and determinants
 * @returns Barycentric weights {aW, bW, cW} if point is inside triangle, undefined otherwise
 * @throws {Error} When point coordinates or cache data are invalid
 * @see {@link assertValidNumber} for coordinate validation details
 */
function calculateBarycentricWeights(point, cache) {
    assertValidNumber(point.x, "point.x");
    assertValidNumber(point.y, "point.y");
    assertValidNumber(cache.origin.x, "cache.origin.x");
    assertValidNumber(cache.origin.y, "cache.origin.y");
    assertValidNumber(cache.min.x, "cache.min.x");
    assertValidNumber(cache.min.y, "cache.min.y");
    assertValidNumber(cache.max.x, "cache.max.x");
    assertValidNumber(cache.max.y, "cache.max.y");
    assertValidNumber(cache.u.x, "cache.u.x");
    assertValidNumber(cache.u.y, "cache.u.y");
    assertValidNumber(cache.v.x, "cache.v.x");
    assertValidNumber(cache.v.y, "cache.v.y");
    assertValidNumber(cache.d00, "cache.d00");
    assertValidNumber(cache.d01, "cache.d01");
    assertValidNumber(cache.d11, "cache.d11");
    assertValidNumber(cache.invDenom, "cache.invDenom");
    if (point.x < cache.min.x ||
        point.x > cache.max.x ||
        point.y < cache.min.y ||
        point.y > cache.max.y) {
        return undefined;
    }
    const originToPoint = {
        x: point.x - cache.origin.x,
        y: point.y - cache.origin.y,
    };
    const d20 = originToPoint.x * cache.u.x + originToPoint.y * cache.u.y;
    const d21 = originToPoint.x * cache.v.x + originToPoint.y * cache.v.y;
    const bW = (cache.d11 * d20 - cache.d01 * d21) * cache.invDenom;
    if (bW < 0) {
        return undefined;
    }
    const cW = (cache.d00 * d21 - cache.d01 * d20) * cache.invDenom;
    if (cW < 0) {
        return undefined;
    }
    const aW = 1 - bW - cW;
    if (aW < 0) {
        return undefined;
    }
    return { aW, bW, cW };
}
/**
 * Calculates the centroid (geometric center) of a triangle.
 * The centroid is the point where all three medians of the triangle intersect.
 *
 * @param a - First vertex of the triangle (Vector2Like coordinates)
 * @param b - Second vertex of the triangle (Vector2Like coordinates)
 * @param c - Third vertex of the triangle (Vector2Like coordinates)
 * @returns The centroid point of the triangle (Vector2Like coordinates)
 * @throws {Error} When any coordinate value is invalid
 * @see {@link assertValidNumber} for coordinate validation details
 */
function calculateTriangleCentroid(a, b, c) {
    assertValidNumber(a.x, "a.x");
    assertValidNumber(a.y, "a.y");
    assertValidNumber(b.x, "b.x");
    assertValidNumber(b.y, "b.y");
    assertValidNumber(c.x, "c.x");
    assertValidNumber(c.y, "c.y");
    return {
        x: (a.x + b.x + c.x) / TRIANGLE_VERTEX_COUNT,
        y: (a.y + b.y + c.y) / TRIANGLE_VERTEX_COUNT,
    };
}
/**
 * Determines if a point is strictly inside a circle (excluding the boundary).
 * Uses squared distance comparison to avoid expensive square root operations.
 *
 * @param origin - The center point of the circle (Vector2Like coordinates)
 * @param radiusSquared - The squared radius of the circle (positive number)
 * @param point - The point to test (Vector2Like coordinates)
 * @returns True if the point is strictly inside the circle, false otherwise
 * @throws {Error} When coordinates are invalid or radius is not positive
 * @see {@link assertValidNumber} for coordinate validation details
 * @see {@link assertValidPositiveNumber} for radius validation details
 */
function isPointInsideCircle(origin, radiusSquared, point) {
    assertValidNumber(origin.x, "origin.x");
    assertValidNumber(origin.y, "origin.y");
    assertValidPositiveNumber(radiusSquared, "radiusSquared");
    assertValidNumber(point.x, "point.x");
    assertValidNumber(point.y, "point.y");
    return ((point.x - origin.x) ** 2 + (point.y - origin.y) ** 2 <
        radiusSquared - EPSILON);
}
/**
 * Normalizes an azimuth angle to the range [0, 2π).
 * Handles negative angles by adding 2π to bring them into the valid range.
 *
 * @param azimuth - The azimuth angle in radians (any finite number)
 * @returns The normalized azimuth in the range [0, 2π) radians
 * @throws {Error} When the azimuth value is not a valid number
 * @see {@link assertValidNumber} for validation details
 */
function calculateNormalizedAzimuth(azimuth) {
    assertValidNumber(azimuth, "Azimuth for normalization");
    const result = azimuth % PI2;
    return result < 0 ? result + PI2 : result;
}
/**
 * Calculates the forward angular distance between two azimuth angles.
 * Always measures distance in the positive direction, even if a shorter path exists in the backward direction.
 *
 * @param from - The starting azimuth angle in radians (normalized to [0, 2π))
 * @param to - The target azimuth angle in radians (normalized to [0, 2π))
 * @returns The forward angular distance in radians [0, 2π)
 * @throws {Error} When either azimuth value is invalid
 * @see {@link assertValidAzimuth} for azimuth validation details
 */
function calculateAngularDistanceForward(from, to) {
    assertValidAzimuth(from, "Azimuth 'from'");
    assertValidAzimuth(to, "Azimuth 'to'");
    const delta = to - from;
    return delta >= 0 ? delta : delta + PI2;
}
/**
 * Determines if an azimuth angle falls within a specified angular range.
 * Handles ranges that wrap around the 0/2π boundary correctly.
 *
 * @param value - The azimuth angle to test in radians (normalized to [0, 2π))
 * @param from - The start of the angular range in radians (normalized to [0, 2π))
 * @param to - The end of the angular range in radians (normalized to [0, 2π))
 * @returns True if the azimuth is within the range (inclusive), false otherwise
 * @throws {Error} When any azimuth value is invalid
 * @see {@link assertValidAzimuth} for azimuth validation details
 */
function isAzimuthBetween(value, from, to) {
    assertValidAzimuth(value, "Azimuth value");
    assertValidAzimuth(from, "Azimuth 'from'");
    assertValidAzimuth(to, "Azimuth 'to'");
    return from <= to
        ? value >= from && value <= to
        : value >= from || value <= to;
}
/**
 * Calculates the squared Euclidean distance between two points.
 * Using squared distance avoids the expensive square root operation.
 *
 * @param x1 - X coordinate of the first point (finite number)
 * @param y1 - Y coordinate of the first point (finite number)
 * @param x2 - X coordinate of the second point (finite number)
 * @param y2 - Y coordinate of the second point (finite number)
 * @returns The squared distance between the two points (non-negative number)
 * @throws {Error} When any coordinate value is invalid
 * @see {@link assertValidNumber} for coordinate validation details
 */
function calculateDistanceSquared(x1, y1, x2, y2) {
    assertValidNumber(x1, "Coordinate 'x1'");
    assertValidNumber(y1, "Coordinate 'y1'");
    assertValidNumber(x2, "Coordinate 'x2'");
    assertValidNumber(y2, "Coordinate 'y2'");
    return (x2 - x1) ** 2 + (y2 - y1) ** 2;
}
/**
 * Calculates the squared distance from a point to a line segment (edge).
 * Projects the point onto the line segment and clamps to the segment boundaries.
 *
 * @param edge - A tuple containing the two endpoints of the edge (Vector2Like coordinates)
 * @param x - X coordinate of the point (finite number)
 * @param y - Y coordinate of the point (finite number)
 * @returns The squared distance from the point to the closest point on the edge (non-negative number)
 * @throws {Error} When any coordinate value is invalid
 * @see {@link assertValidNumber} for coordinate validation details
 */
function calculateDistanceToEdgeSquared([p1, p2], x, y) {
    assertValidNumber(p1.x, "Coordinate 'p1.x'");
    assertValidNumber(p1.y, "Coordinate 'p1.y'");
    assertValidNumber(p2.x, "Coordinate 'p2.x'");
    assertValidNumber(p2.y, "Coordinate 'p2.y'");
    assertValidNumber(x, "Coordinate 'x'");
    assertValidNumber(y, "Coordinate 'y'");
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const t = ((x - p1.x) * dx + (y - p1.y) * dy) / (dx * dx + dy * dy);
    const clampedT = t <= 0 ? 0 : t >= 1 ? 1 : t;
    const deltaX = x - (p1.x + clampedT * dx);
    const deltaY = y - (p1.y + clampedT * dy);
    return deltaX ** 2 + deltaY ** 2;
}

/** Scale factor for generating the super triangle that encompasses all input points. */
const SUPER_TRIANGLE_SCALE_FACTOR = 16;
/**
 * Implements Bowyer-Watson algorithm for Delaunay triangulation in 2D space.
 * Generates a triangulation where no point lies inside the circumcircle of any triangle.
 */
class DelaunayTriangulator {
    /**
     * Performs Delaunay triangulation on a set of 2D points using the Bowyer-Watson algorithm.
     * The resulting triangulation satisfies the Delaunay condition: no point lies inside
     * the circumcircle of any triangle.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param points - Array of points to triangulate (minimum 3 points required)
     * @returns Triangulation result containing triangles and boundary edge information
     * @throws {Error} When fewer than 3 points provided, points contain invalid coordinates,
     *                 duplicate points exist, or all points are collinear
     * @see {@link TriangulationResult} for result structure details
     * @see {@link Triangle} for triangle structure details
     */
    static triangulate(points) {
        if (points.length < TRIANGLE_VERTEX_COUNT) {
            throw new Error(`At least ${TRIANGLE_VERTEX_COUNT} points are required for triangulation, but got ${points.length}`);
        }
        for (const point of points) {
            assertValidNumber(point.x, "x-coordinate");
            assertValidNumber(point.y, "y-coordinate");
        }
        for (let i = 0; i < points.length - 1; i++) {
            const x = points[i].x;
            const y = points[i].y;
            for (let j = i + 1; j < points.length; j++) {
                if (Math.abs(x - points[j].x) < EPSILON &&
                    Math.abs(y - points[j].y) < EPSILON) {
                    throw new Error(`Duplicate points found at indices ${i} and ${j}: (${x}, ${y})`);
                }
            }
        }
        {
            const [a, b, ...rest] = points;
            const direction = new Vector2().subVectors(b, a).normalize();
            const temp = new Vector2();
            if (rest.every((p) => Math.abs(direction.cross(temp.subVectors(p, a))) < EPSILON)) {
                throw new Error("All points are collinear - cannot run triangulation");
            }
        }
        const superTriangle = DelaunayTriangulator.buildSuperTriangle(points);
        let triangles = [superTriangle];
        for (const point of points) {
            const badTriangles = DelaunayTriangulator.filterBadTriangles(triangles, point);
            for (const edge of DelaunayTriangulator.buildPolygon(badTriangles)) {
                triangles.push({
                    a: edge[0],
                    b: edge[1],
                    c: point,
                    ...precomputeTriangle(edge[0], edge[1], point),
                });
            }
        }
        const boundaryEdgeMap = DelaunayTriangulator.filterSuperTriangleVertices(triangles, superTriangle);
        for (const [key, value] of boundaryEdgeMap) {
            // This code is unreachable under all inputs.
            // The map represents the outer contour of a closed 2D mesh after Delaunay triangulation.
            // Every boundary vertex must have exactly two neighbors on the contour.
            // If this branch is ever taken, it implies a violation of mesh invariants and a bug elsewhere in the pipeline —
            // not an edge case to be covered by tests.
            /* c8 ignore next 5 */
            if (value.length !== 2) {
                throw new Error(`Invariant violation: outer edge point (${key.x}, ${key.y}) has ${value.length} connections (expected 2)`);
            }
        }
        return { triangles, boundaryEdgeMap: boundaryEdgeMap };
    }
    /**
     * Identifies and removes triangles whose circumcircles contain the given point.
     * Such triangles violate the Delaunay condition and must be removed.
     * Modifies the input triangles array in-place for efficiency.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param triangles - Array of triangles to filter (modified in-place)
     * @param point - Point to test against triangle circumcircles (Vector2Like coordinates)
     * @returns Array of triangles that contain the point in their circumcircles
     * @see {@link isPointInsideCircle} for circumcircle containment test
     */
    static filterBadTriangles(triangles, point) {
        const badTriangles = [];
        let w = 0;
        for (const triangle of triangles) {
            const isBadTriangle = isPointInsideCircle(triangle.circumcenter, triangle.circumradiusSquared, point);
            if (isBadTriangle) {
                badTriangles.push(triangle);
            }
            else {
                triangles[w++] = triangle;
            }
        }
        triangles.length = w;
        return badTriangles;
    }
    /**
     * Constructs the polygon boundary formed by removing bad triangles.
     * Finds edges that belong to only one triangle (outer edges of the cavity).
     * These edges will form the boundary of the polygon hole left by removed triangles.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param triangles - Array of triangles forming the cavity (finite array)
     * @returns Array of edges [vertex1, vertex2] that form the polygon boundary
     */
    static buildPolygon(triangles) {
        const polygonOuterEdges = [];
        for (const triangle of triangles) {
            const edges = [
                [triangle.a, triangle.b],
                [triangle.b, triangle.c],
                [triangle.c, triangle.a],
            ];
            for (const edge of edges) {
                const isOuterEdge = triangles.every((t) => triangle === t ||
                    [
                        [t.a, t.b],
                        [t.b, t.c],
                        [t.c, t.a],
                    ].every((e) => (edge[0] !== e[0] || edge[1] !== e[1]) &&
                        (edge[1] !== e[0] || edge[0] !== e[1])));
                if (isOuterEdge) {
                    polygonOuterEdges.push(edge);
                }
            }
        }
        return polygonOuterEdges;
    }
    /**
     * Removes triangles containing super triangle vertices and builds boundary edge map.
     * Filters out triangles that include any vertex from the initial super triangle,
     * keeping only triangles formed entirely from input points. Also constructs
     * a map of boundary vertices to their adjacent boundary vertices.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param triangles - Array of triangles to filter (modified in-place)
     * @param superTriangle - The super triangle used to initialize triangulation
     * @returns Map from boundary vertices to arrays of their adjacent boundary vertices
     * @throws {Error} When boundary vertex connectivity invariants are violated
     */
    static filterSuperTriangleVertices(triangles, superTriangle) {
        const boundaryEdgeMap = new Map();
        let w = 0;
        for (const t of triangles) {
            const boundaryPoints = [];
            if (!DelaunayTriangulator.isVertexFromTriangle(t.a, superTriangle)) {
                boundaryPoints.push(t.a);
            }
            if (!DelaunayTriangulator.isVertexFromTriangle(t.b, superTriangle)) {
                boundaryPoints.push(t.b);
            }
            if (!DelaunayTriangulator.isVertexFromTriangle(t.c, superTriangle)) {
                boundaryPoints.push(t.c);
            }
            if (boundaryPoints.length === 2) {
                const [lPoint, rPoint] = boundaryPoints;
                const lArray = boundaryEdgeMap.get(lPoint) ?? [];
                const rArray = boundaryEdgeMap.get(rPoint) ?? [];
                lArray.push(rPoint);
                rArray.push(lPoint);
                boundaryEdgeMap.set(lPoint, lArray);
                boundaryEdgeMap.set(rPoint, rArray);
            }
            if (boundaryPoints.length === TRIANGLE_VERTEX_COUNT) {
                triangles[w++] = t;
            }
        }
        triangles.length = w;
        return boundaryEdgeMap;
    }
    /**
     * Constructs a large triangle that encompasses all input points.
     * The super triangle provides an initial triangulation that contains all points,
     * allowing the incremental algorithm to proceed. Vertices are positioned far
     * outside the bounding box of input points.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param points - Array of input points to encompass (finite coordinates)
     * @returns Triangle that contains all input points with precomputed properties
     * @see {@link precomputeTriangle} for triangle property computation
     */
    static buildSuperTriangle(points) {
        let minX = Infinity;
        let minY = Infinity;
        let maxX = -Infinity;
        let maxY = -Infinity;
        for (const point of points) {
            minX = Math.min(minX, point.x);
            minY = Math.min(minY, point.y);
            maxX = Math.max(maxX, point.x);
            maxY = Math.max(maxY, point.y);
        }
        const delta = Math.max(maxX - minX, maxY - minY) * SUPER_TRIANGLE_SCALE_FACTOR;
        const a = { x: minX - delta, y: minY - delta };
        const b = { x: minX + 2 * delta, y: minY - delta };
        const c = { x: minX + delta, y: minY + 2 * delta };
        return { a, b, c, ...precomputeTriangle(a, b, c) };
    }
    /**
     * Tests whether a vertex belongs to a specific triangle.
     * Uses reference equality to determine if the vertex is one of the triangle's vertices.
     *
     * @template T - Type of vertices that must be Vector2Like objects
     * @param vertex - Vertex to test for membership (Vector2Like coordinates)
     * @param triangle - Triangle to test against
     * @returns True if the vertex is one of the triangle's vertices, false otherwise
     */
    static isVertexFromTriangle(vertex, triangle) {
        return (vertex === triangle.a || vertex === triangle.b || vertex === triangle.c);
    }
}

/**
 * Abstract base class for animation trees in the animation state machine.
 * Provides foundation for different tree types (linear, polar, freeform, etc.)
 * and manages animation anchors with their weights.
 *
 * Animation trees organize and control multiple animation actions through
 * a hierarchical structure, automatically managing playback and weight distribution.
 */
class AnimationTree extends AnimationState {
    constructor() {
        super(...arguments);
        /**
         * Map linking animation actions to their corresponding anchors.
         * Used to look up anchor information when registering time-based events.
         */
        this.actionToAnchor = new Map();
    }
    /**
     * Registers a callback to be called when the specified animation action reaches a specific time.
     * The callback will be invoked every time the animation crosses the specified time threshold.
     *
     * @param action - The animation action to monitor for time events
     * @param unitTime - Time in unit range [0, 1] when the callback should be invoked
     * @param callback - Function to call when the time event occurs, receives the action and state as parameters
     * @throws {Error} When the action is not registered in this animation tree
     */
    onTimeEvent(action, unitTime, callback) {
        const anchor = this.actionToAnchor.get(action);
        if (!anchor) {
            throw new Error(`Action is not registered`);
        }
        this.onTimeEventInternal(anchor, unitTime, callback, false);
    }
    /**
     * Registers a callback to be called once when the specified animation action reaches a specific time.
     * The callback will be invoked only the first time the animation crosses the specified time threshold.
     *
     * @param action - The animation action to monitor for time events
     * @param unitTime - Time in unit range [0, 1] when the callback should be invoked
     * @param callback - Function to call when the time event occurs, receives the action and state as parameters
     * @throws {Error} When the action is not registered in this animation tree
     */
    onceTimeEvent(action, unitTime, callback) {
        const anchor = this.actionToAnchor.get(action);
        if (!anchor) {
            throw new Error(`Action is not registered`);
        }
        this.onTimeEventInternal(anchor, unitTime, callback, true);
    }
    /**
     * Removes a previously registered time event callback for the specified animation action.
     * Unregisters the callback from the specified time point and cleans up associated resources.
     *
     * @param action - The animation action to remove the time event from
     * @param unitTime - Time in unit range [0, 1] where the callback was registered
     * @param callback - The callback function to remove
     * @throws {Error} When the action is not registered in this animation tree
     */
    offTimeEvent(action, unitTime, callback) {
        const anchor = this.actionToAnchor.get(action);
        if (!anchor) {
            throw new Error(`Action is not registered`);
        }
        this.offTimeEventInternal(anchor, unitTime, callback);
    }
    /**
     * Internal method to set the influence of this animation tree.
     * When influence changes, triggers an update of anchor influences while
     * maintaining their relative weights unchanged.
     *
     * @param influence - The new influence value in range [0, 1]
     * @throws {Error} When influence is not a finite number or is outside the range [0, 1]
     * @internal This method is intended to be called only by the animation state machine
     */
    ["setInfluenceInternal"](influence) {
        assertValidUnitRange(influence, "Animation tree influence");
        if (influence !== this.influenceInternal) {
            this.influenceInternal = influence;
            this.updateAnchorsInfluence();
        }
    }
    /**
     * Updates the weight of a specific animation anchor (AnimationAction + parameters).
     * Handles animation playbook lifecycle: starting, stopping, and weight adjustments.
     * Combines the raw weight with the tree's influence to get the final action weight.
     *
     * When transitioning from zero to non-zero weight: starts playback, resets time to 0,
     * resets event tracking state, and emits PLAY event.
     * When transitioning from non-zero to zero weight: stops playback, resets time to 0,
     * resets event tracking state, and emits STOP event.
     * For weight-only changes: updates the animation action weight without lifecycle changes.
     *
     * @param anchor - The animation anchor containing the action and parameters to update
     * @param weight - The raw weight value before applying tree influence (finite number). If not provided, uses the anchor's current weight
     * @throws {Error} When weight is not a finite number or is outside the range [0, 1]
     * @see {@link AnimationStateEvent.PLAY} for play event details
     * @see {@link AnimationStateEvent.STOP} for stop event details
     */
    updateAnchorWeight(anchor, weight = anchor.weight) {
        assertValidUnitRange(weight, "Anchor weight");
        anchor.weight = weight;
        const combinedWeight = weight * this.influenceInternal;
        const animationAction = anchor.action;
        if (combinedWeight === animationAction.weight) {
            return;
        }
        if (combinedWeight > 0 && animationAction.weight === 0) {
            animationAction.enabled = true;
            animationAction.paused = false;
            animationAction.time = 0;
            animationAction.play();
            this.emit(AnimationStateEvent.PLAY, animationAction, this);
        }
        else if (combinedWeight === 0 && animationAction.weight > 0) {
            animationAction.stop();
            animationAction.time = 0;
            animationAction.paused = false;
            animationAction.enabled = false;
            this.emit(AnimationStateEvent.STOP, animationAction, this);
        }
        animationAction.weight = combinedWeight;
    }
}

/** Minimum number of actions required for freeform blend tree triangulation */
const MIN_ACTION_COUNT = 3;
/**
 * Freeform blend tree implementation for arbitrary 2D animation blending.
 *
 * Uses Delaunay triangulation to create a mesh from animation actions positioned
 * in arbitrary 2D coordinates, enabling smooth interpolation between animations
 * based on barycentric coordinates within triangles. For points outside the mesh,
 * falls back to nearest edge interpolation for boundary handling.
 *
 * The blend tree automatically constructs a triangulated mesh from input actions
 * and provides seamless blending across the entire 2D space, making it ideal
 * for complex animation systems like movement with multiple speed/direction
 * combinations or facial animation with arbitrary control points.
 *
 * @example Character Movement with Arbitrary Speeds
 * ```typescript
 * // Define movement animations at various speeds and directions
 * const actions = [
 *   { action: idleAction, x: 0, y: 0 },           // Center: idle
 *   { action: walkNorthAction, x: 0, y: 1 },      // North: slow
 *   { action: runNorthAction, x: 0, y: 2 },       // North: fast
 *   { action: walkEastAction, x: 1, y: 0 },       // East: slow
 *   { action: runEastAction, x: 2, y: 0 },        // East: fast
 *   { action: walkNEAction, x: 0.7, y: 0.7 },     // Northeast: diagonal
 *   { action: sprintAction, x: 1.5, y: 1.5 }      // Sprint: very fast diagonal
 * ];
 *
 * const blendTree = new FreeformBlendTree(actions);
 *
 * // Blend to medium speed northeast
 * blendTree.setBlend(0.5, 0.8);
 *
 * // Blend to maximum speed due east
 * blendTree.setBlend(2.0, 0.0);
 * ```
 */
class FreeformBlendTree extends AnimationTree {
    /**
     * Creates a new freeform blend tree from animation actions positioned in 2D space.
     * Performs Delaunay triangulation to create a mesh for barycentric interpolation.
     * Initializes all actions to stopped state and validates coordinates and durations.
     *
     * @param freeformActions - Array of freeform actions defining the blend space.
     *                         Must contain at least 3 actions with unique coordinates.
     * @throws {Error} When fewer than 3 actions are provided
     * @throws {Error} When any action has non-finite coordinates
     * @throws {Error} When any action coordinates are outside JavaScript's safe range
     * @throws {Error} When multiple actions have the same coordinates
     * @throws {Error} When any animation clip duration is not positive
     * @throws {Error} When actions form degenerate triangulation (all collinear)
     * @see {@link assertValidNumber} for coordinate validation details
     * @see {@link DelaunayTriangulator.triangulate} for triangulation details
     */
    constructor(freeformActions) {
        super();
        this._private_tempAnchorMap = new Map();
        this._private_trackableAnchors = [];
        this._private_triangles = [];
        this._private_boundaryEdgeMap = new Map();
        this._private_currentX = 0;
        this._private_currentY = 0;
        if (freeformActions.length < MIN_ACTION_COUNT) {
            throw new Error("FreeformBlendTree requires at least 3 actions for triangulation");
        }
        for (let i = 0; i < freeformActions.length; i++) {
            assertValidNumber(freeformActions[i].x, `Freeform action at index ${i} x value`);
            assertValidNumber(freeformActions[i].y, `Freeform action at index ${i} y value`);
        }
        for (let i = 0; i < freeformActions.length - 1; i++) {
            const x = freeformActions[i].x;
            const y = freeformActions[i].y;
            for (let j = i + 1; j < freeformActions.length; j++) {
                if (Math.abs(x - freeformActions[j].x) < EPSILON &&
                    Math.abs(y - freeformActions[j].y) < EPSILON) {
                    throw new Error(`Duplicate coordinates found, x: ${x}, y: ${y}. All action values must be unique.`);
                }
            }
        }
        const anchors = [];
        for (const freeformAction of freeformActions) {
            const animationAction = freeformAction.action;
            animationAction.stop();
            animationAction.time = 0;
            animationAction.weight = 0;
            animationAction.paused = false;
            animationAction.enabled = false;
            const duration = animationAction.getClip().duration;
            if (duration <= 0) {
                throw new Error("Action duration must be greater than zero");
            }
            const anchor = {
                index: getNextAnchorIndex(),
                action: animationAction,
                weight: 0,
                duration,
                invDuration: 1 / duration,
                iterationEventType: animationAction.loop === LoopOnce
                    ? AnimationStateEvent.FINISH
                    : AnimationStateEvent.ITERATE,
                x: freeformAction.x,
                y: freeformAction.y,
            };
            anchors.push(anchor);
            this.actionToAnchor.set(animationAction, anchor);
        }
        const result = DelaunayTriangulator.triangulate(anchors);
        this._private_triangles = result.triangles.map((t) => {
            // Exclude 'circumcenter' and 'circumradiusSquared' from 't' — we don't need them in the result.
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const { circumcenter, circumradiusSquared, ...rest } = t;
            return {
                ...rest,
                centroid: calculateTriangleCentroid(t.a, t.b, t.c),
            };
        });
        this._private_boundaryEdgeMap = result.boundaryEdgeMap;
        this._private_sortTriangles();
        this._private_updateAnchors();
    }
    get blendValue() {
        return { x: this._private_currentX, y: this._private_currentY };
    }
    /**
     * Sets the blend position in 2D Cartesian coordinates to determine animation weights.
     * When the position changes, animation weights are recalculated using barycentric
     * interpolation within triangles or nearest edge interpolation for boundary points.
     *
     * Points inside triangles use barycentric coordinates for smooth 3-way blending.
     * Points outside the mesh use interpolation along the nearest boundary edge.
     *
     * @param x - Target X coordinate in 2D space (finite number)
     * @param y - Target Y coordinate in 2D space (finite number)
     * @throws {Error} When x coordinate is not a finite number
     * @throws {Error} When y coordinate is not a finite number
     * @see {@link assertValidNumber} for coordinate validation details
     *
     * @example
     * ```typescript
     * // Blend to position within the mesh
     * blendTree.setBlend(1.2, 0.8);
     *
     * // Blend to position outside mesh (uses boundary interpolation)
     * blendTree.setBlend(-0.5, 3.0);
     * ```
     */
    setBlend(x, y) {
        assertValidNumber(x, "Blend x");
        assertValidNumber(y, "Blend y");
        if (this._private_currentX !== x || this._private_currentY !== y) {
            this._private_currentX = x;
            this._private_currentY = y;
            this._private_sortTriangles();
            this._private_updateAnchors();
        }
    }
    /**
     * Internal method called by the animation state machine on each frame update.
     * Tracks animation progress and emits iteration events for all triangulated actions.
     * Monitors timing changes across all anchors in the triangulated mesh.
     *
     * @internal This method is called exclusively by the animation state machine
     * @see {@link updateAnchorTime} for time tracking and event emission details
     */
    ["onTickInternal"](deltaTime) {
        if (this.influence === 0) {
            throw new Error(`${this.name}: cannot update anchor time because the animation influence is zero`);
        }
        for (const anchor of this._private_trackableAnchors) {
            this.processTimeEvents(anchor, deltaTime);
            this.updateAnchorTime(anchor, deltaTime);
        }
    }
    ["onEnterInternal"]() {
        super.onEnterInternal();
        if (this.influence > 0) {
            for (const anchor of this._private_trackableAnchors) {
                this.resetFinishedAction(anchor);
            }
        }
    }
    /**
     * Updates the influence for all anchors in the freeform blend tree.
     * Called when the tree's influence changes but relative weights remain the same.
     * Applies the current tree influence to all triangulated anchors while maintaining
     * their existing weight distribution from the freeform blending calculations.
     */
    updateAnchorsInfluence() {
        for (const anchor of this._private_trackableAnchors) {
            this.updateAnchorWeight(anchor);
        }
    }
    /**
     * Recalculates and updates animation weights based on current blend position.
     *
     * This is the core blending algorithm that:
     * 1. Attempts barycentric interpolation if point lies within any triangle
     * 2. Falls back to nearest boundary edge interpolation for external points
     * 3. Updates the active anchors set with calculated weights
     *
     * The method prioritizes smooth 3-way barycentric blending when possible,
     * providing seamless 2-way edge blending for boundary cases.
     */
    _private_updateAnchors() {
        this._private_tempAnchorMap.clear();
        for (const anchor of this._private_trackableAnchors) {
            this._private_tempAnchorMap.set(anchor, 0);
        }
        if (!this._private_applyBarycentricWeights(this._private_tempAnchorMap)) {
            this._private_applyNearestNeighborWeight(this._private_tempAnchorMap);
        }
        this._private_trackableAnchors.length = 0;
        for (const [anchor, weight] of this._private_tempAnchorMap) {
            this.updateAnchorWeight(anchor, weight);
            if (weight > 0) {
                this._private_trackableAnchors.push(anchor);
            }
        }
    }
    /**
     * Attempts to apply barycentric weights if the blend point lies within any triangle.
     * Searches through all triangles to find one containing the current blend position
     * and calculates the barycentric coordinates for 3-way interpolation.
     *
     * @param result - Map to store calculated weights for each anchor
     * @returns True if barycentric interpolation was applied, false if point is outside all triangles
     * @see {@link calculateBarycentricWeights} for barycentric coordinate calculation
     */
    _private_applyBarycentricWeights(result) {
        const point = { x: this._private_currentX, y: this._private_currentY };
        for (const triangle of this._private_triangles) {
            const weights = calculateBarycentricWeights(point, triangle);
            if (weights) {
                result.set(triangle.a, weights.aW);
                result.set(triangle.b, weights.bW);
                result.set(triangle.c, weights.cW);
                return true;
            }
        }
        return false;
    }
    /**
     * Applies nearest boundary edge interpolation for points outside the triangulated mesh.
     * Finds the closest boundary vertex and interpolates along the nearest boundary edge
     * to provide smooth 2-way blending for external points.
     *
     * The algorithm:
     * 1. Finds the closest anchor among all boundary vertices
     * 2. Identifies the two boundary edges connected to this anchor
     * 3. Determines which edge is closer to the blend point
     * 4. Projects the point onto the edge and calculates interpolation weights
     *
     * @param result - Map to store calculated weights for each anchor
     * @see {@link calculateDistanceSquared} for distance calculations
     * @see {@link calculateDistanceToEdgeSquared} for edge distance calculations
     */
    _private_applyNearestNeighborWeight(result) {
        const nearestTriangle = this._private_triangles[0];
        const closestAnchor = [
            nearestTriangle.a,
            nearestTriangle.b,
            nearestTriangle.c,
        ].reduce((a, b) => calculateDistanceSquared(a.x, a.y, this._private_currentX, this._private_currentY) <
            calculateDistanceSquared(b.x, b.y, this._private_currentX, this._private_currentY)
            ? a
            : b);
        const edgeData = this._private_boundaryEdgeMap.get(closestAnchor);
        // This code is unreachable under all inputs.
        // Every outer contour vertex must have a corresponding entry in boundaryEdgeMap.
        // If this branch is ever taken, it implies a violation of mesh construction invariants —
        // most likely a logic error in how the boundary map was built or queried.
        // This is not an edge case to be tested, but a correctness bug upstream.
        /* c8 ignore next 5 */
        if (edgeData === undefined) {
            throw new Error(`Invariant violation: no edge data found for outer vertex (${closestAnchor.x}, ${closestAnchor.y})`);
        }
        const edge0 = [
            closestAnchor,
            edgeData[0],
        ];
        const edge1 = [
            closestAnchor,
            edgeData[1],
        ];
        const closestEdge = calculateDistanceToEdgeSquared(edge0, this._private_currentX, this._private_currentY) <
            calculateDistanceToEdgeSquared(edge1, this._private_currentX, this._private_currentY)
            ? edge0
            : edge1;
        const [a, b] = closestEdge;
        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const lengthSquared = dx * dx + dy * dy;
        const value = ((this._private_currentX - a.x) * dx + (this._private_currentY - a.y) * dy) / lengthSquared;
        const t = Math.max(0, Math.min(1, value));
        result.set(a, 1 - t);
        result.set(b, t);
    }
    /**
     * Sorts triangles by distance from their centroids to the current blend position.
     * This optimization improves performance of barycentric weight calculation by
     * checking closer triangles first, reducing average search time.
     *
     * Called whenever the blend position changes to maintain optimal search order.
     */
    _private_sortTriangles() {
        this._private_triangles.sort((a, b) => calculateDistanceSquared(a.centroid.x, a.centroid.y, this._private_currentX, this._private_currentY) -
            calculateDistanceSquared(b.centroid.x, b.centroid.y, this._private_currentX, this._private_currentY));
    }
}

/**
 * Linear blend tree implementation for 1D animation blending.
 *
 * Manages a collection of animation actions positioned along a linear axis,
 * automatically blending between adjacent animations based on a blend value.
 * The blend tree interpolates weights between the two closest animations
 * to create smooth transitions across the linear space.
 *
 * Note: Action values are not limited to the 0-1 range. You can use any
 * numeric values, including negative values, that make sense for your
 * application (e.g., speed in m/s, or any custom metric). However, all
 * values must be finite numbers within JavaScript's safe range and must
 * be unique (no duplicate values allowed).
 *
 * @example
 * ```typescript
 * const idleAction = mixer.clipAction(idleClip);
 * const walkAction = mixer.clipAction(walkClip);
 * const runAction = mixer.clipAction(runClip);
 *
 * const blendTree = new LinearBlendTree([
 *   { action: idleAction, value: 0 },
 *   { action: walkAction, value: 0.5 },
 *   { action: runAction, value: 1 }
 * ]);
 *
 * // Set blend to 0.3 - results in:
 * // idleAction weight: 0.4 (40%)
 * // walkAction weight: 0.6 (60%)
 * // runAction weight: 0.0 (0%)
 * blendTree.setBlend(0.3);
 * ```
 */
class LinearBlendTree extends AnimationTree {
    /**
     * Creates a new linear blend tree with the specified animation actions.
     * Actions are automatically sorted by their value along the linear axis.
     * Initializes all actions to stopped state and validates clip durations.
     *
     * @param linearActions - Array of linear actions defining the blend space.
     *                       Must contain at least 2 actions with unique, finite values.
     * @throws {Error} When fewer than 2 actions are provided
     * @throws {Error} When any action has a non-finite value (NaN, ±Infinity)
     * @throws {Error} When any action has a value outside JavaScript's safe integer range
     * @throws {Error} When multiple actions have the same value (duplicate values)
     * @throws {Error} When any animation clip duration is not a positive finite number
     * @see {@link assertValidNumber} for value validation details
     * @see {@link assertValidPositiveNumber} for duration validation details
     */
    constructor(linearActions) {
        super();
        this._private_anchors = [];
        this._private_currentBlend = 0;
        if (linearActions.length < 2) {
            throw new Error("Need at least 2 actions");
        }
        for (let i = 0; i < linearActions.length; i++) {
            const value = linearActions[i].value;
            assertValidNumber(value, `Linear action at index ${i} value`);
        }
        for (let i = 0; i < linearActions.length - 1; i++) {
            const value = linearActions[i].value;
            for (let j = i + 1; j < linearActions.length; j++) {
                if (Math.abs(value - linearActions[j].value) < EPSILON) {
                    throw new Error(`Duplicate value found, value: ${value}. All action values must be unique.`);
                }
            }
        }
        for (const linearAction of linearActions) {
            const animationAction = linearAction.action;
            animationAction.stop();
            animationAction.time = 0;
            animationAction.weight = 0;
            animationAction.paused = false;
            animationAction.enabled = false;
            const duration = animationAction.getClip().duration;
            assertValidPositiveNumber(duration, "Clip duration");
            const anchor = {
                index: getNextAnchorIndex(),
                action: animationAction,
                weight: 0,
                duration,
                invDuration: 1 / duration,
                iterationEventType: animationAction.loop === LoopOnce
                    ? AnimationStateEvent.FINISH
                    : AnimationStateEvent.ITERATE,
                value: linearAction.value,
            };
            this._private_anchors.push(anchor);
            this.actionToAnchor.set(animationAction, anchor);
        }
        this._private_anchors.sort((a, b) => a.value - b.value);
        this._private_updateAnchors();
    }
    get blendValue() {
        return this._private_currentBlend;
    }
    /**
     * Sets the blend value to determine animation weights along the linear axis.
     * When the blend changes, animation weights are recalculated to interpolate
     * between the two closest actions. Values outside the action range are
     * handled by giving full weight to the nearest boundary action.
     *
     * @param value - The target blend value (finite number)
     * @throws {Error} When the blend value is not a finite number
     * @see {@link assertValidNumber} for value validation details
     */
    setBlend(value) {
        assertValidNumber(value, "Blend value");
        if (value !== this._private_currentBlend) {
            this._private_currentBlend = value;
            this._private_updateAnchors();
        }
    }
    /**
     * Internal method called by the animation state machine on each frame update.
     * Tracks animation progress and emits iteration events when animations
     * complete or restart. Monitors all active anchors for timing changes.
     *
     * @internal This method is called exclusively by the animation state machine
     * @see {@link updateAnchorTime} for time tracking and event emission details
     */
    ["onTickInternal"](deltaTime) {
        if (this.influence === 0) {
            throw new Error(`${this.name}: cannot update anchor time because the animation influence is zero`);
        }
        if (this._private_lastLeftAnchor?.weight) {
            this.processTimeEvents(this._private_lastLeftAnchor, deltaTime);
            this.updateAnchorTime(this._private_lastLeftAnchor, deltaTime);
        }
        if (this._private_lastRightAnchor?.weight) {
            this.processTimeEvents(this._private_lastRightAnchor, deltaTime);
            this.updateAnchorTime(this._private_lastRightAnchor, deltaTime);
        }
    }
    ["onEnterInternal"]() {
        super.onEnterInternal();
        if (this.influence > 0) {
            if (this._private_lastLeftAnchor) {
                this.resetFinishedAction(this._private_lastLeftAnchor);
            }
            if (this._private_lastRightAnchor) {
                this.resetFinishedAction(this._private_lastRightAnchor);
            }
        }
    }
    /**
     * Updates the influence for all anchors in the linear blend tree.
     * Called when the tree's influence changes but relative weights remain the same.
     * Applies the current tree influence to all anchors while maintaining
     * their existing weight distribution from the linear blending.
     */
    updateAnchorsInfluence() {
        if (this._private_lastLeftAnchor) {
            this.updateAnchorWeight(this._private_lastLeftAnchor);
        }
        if (this._private_lastRightAnchor) {
            this.updateAnchorWeight(this._private_lastRightAnchor);
        }
    }
    /**
     * Recalculates and updates animation weights based on the current blend value.
     * Performs linear interpolation between the two actions closest to the blend point.
     * Actions outside the interpolation range receive zero weight.
     *
     * The interpolation uses the formula:
     * - Left weight = 1 - difference
     * - Right weight = difference
     * Where difference = (blend - leftValue) / (rightValue - leftValue)
     */
    _private_updateAnchors() {
        const firstAnchor = this._private_anchors[0];
        if (this._private_currentBlend <= firstAnchor.value) {
            if (this._private_lastLeftAnchor && this._private_lastLeftAnchor !== firstAnchor) {
                this.updateAnchorWeight(this._private_lastLeftAnchor, 0);
            }
            if (this._private_lastRightAnchor) {
                this.updateAnchorWeight(this._private_lastRightAnchor, 0);
            }
            this.updateAnchorWeight(firstAnchor, 1);
            this._private_lastLeftAnchor = firstAnchor;
            this._private_lastRightAnchor = undefined;
            return;
        }
        const lastAnchor = this._private_anchors[this._private_anchors.length - 1];
        if (this._private_currentBlend >= lastAnchor.value) {
            if (this._private_lastRightAnchor && this._private_lastRightAnchor !== lastAnchor) {
                this.updateAnchorWeight(this._private_lastRightAnchor, 0);
            }
            if (this._private_lastLeftAnchor) {
                this.updateAnchorWeight(this._private_lastLeftAnchor, 0);
            }
            this.updateAnchorWeight(lastAnchor, 1);
            this._private_lastRightAnchor = lastAnchor;
            this._private_lastLeftAnchor = undefined;
            return;
        }
        {
            let l = 1;
            let r = this._private_anchors.length - 1;
            while (l < r) {
                const m = (l + r) >>> 1;
                this._private_anchors[m].value < this._private_currentBlend ? (l = m + 1) : (r = m);
            }
            const lAnchor = this._private_anchors[l - 1];
            const rAnchor = this._private_anchors[l];
            if (this._private_lastLeftAnchor &&
                this._private_lastLeftAnchor !== lAnchor &&
                this._private_lastLeftAnchor !== rAnchor) {
                this.updateAnchorWeight(this._private_lastLeftAnchor, 0);
            }
            if (this._private_lastRightAnchor &&
                this._private_lastRightAnchor !== lAnchor &&
                this._private_lastRightAnchor !== rAnchor) {
                this.updateAnchorWeight(this._private_lastRightAnchor, 0);
            }
            const difference = (this._private_currentBlend - lAnchor.value) / (rAnchor.value - lAnchor.value);
            this.updateAnchorWeight(lAnchor, 1 - difference);
            this.updateAnchorWeight(rAnchor, difference);
            this._private_lastLeftAnchor = lAnchor;
            this._private_lastRightAnchor = rAnchor;
        }
    }
}

/** Minimum number of actions required for polar blend tree triangulation */
const MIN_POLAR_ACTIONS = 2;
/**
 * Polar blend tree implementation for 2D animation blending in polar coordinates.
 *
 * This class manages a collection of animation actions positioned in polar space (radius, azimuth),
 * automatically blending between adjacent animations based on polar coordinates. The blend tree
 * organizes actions into rays (constant azimuth) and rings (constant radius) to enable efficient
 * bilinear interpolation between the four closest animations, creating smooth transitions across
 * the polar space.
 *
 * ## Architecture
 * - **Rays**: Groups of anchors with the same azimuth but different radii
 * - **Rings**: Groups of anchors with the same radius but different azimuths
 * - **Bilinear Interpolation**: Weight calculation between 4 corner anchors in polar grid
 * - **Center Action**: Optional action at origin (0,0) for special handling
 *
 * ## Coordinate System
 * - **Radius**: Distance from origin, must be non-negative
 * - **Azimuth**: Angle in radians, automatically normalized to [0, 2π) range
 * - **Origin**: Special point (0,0) handled separately if center action provided
 *
 * ## Input Validation
 * - Minimum 2 actions required for basic interpolation
 * - All radius values must be finite, positive, within JavaScript safe range
 * - All azimuth values must be finite, within JavaScript safe range
 * - No duplicate polar coordinates allowed
 * - At least 2 rays required (different azimuth values)
 * - All rays must have the same number of anchors for grid consistency
 *
 * ## Blending Algorithm
 * 1. Find the two adjacent rays that contain the target azimuth
 * 2. Calculate angular interpolation weights between these rays
 * 3. For each ray, find the two adjacent rings containing the target radius
 * 4. Calculate radial interpolation weights between these rings
 * 5. Apply bilinear interpolation to determine final animation weights
 * 6. Handle special cases for center action and edge boundaries
 *
 * @example Walk/Run Directional Movement
 * ```typescript
 * // Create actions for 4-directional movement at two speeds
 * const idle = mixer.clipAction(idleAnimationClip);
 *
 * // Walk actions (radius = 1)
 * const walkForward = mixer.clipAction(walkForwardAnimationClip);
 * const walkRight = mixer.clipAction(walkRightAnimationClip);
 * const walkBackward = mixer.clipAction(walkBackwardAnimationClip);
 * const walkLeft = mixer.clipAction(walkLeftAnimationClip);
 *
 * // Run actions (radius = 2)
 * const runForward = mixer.clipAction(runForwardAnimationClip);
 * const runRight = mixer.clipAction(runRightAnimationClip);
 * const runBackward = mixer.clipAction(runBackwardAnimationClip);
 * const runLeft = mixer.clipAction(runLeftAnimationClip);
 *
 * // Set up polar blend tree with 4-directional movement
 * const blendTree = new PolarBlendTree([
 *   // Walk speed (radius = 1)
 *   { action: walkForward, radius: 1, azimuth: MathUtils.degToRad(0) },    // Forward
 *   { action: walkLeft, radius: 1, azimuth: MathUtils.degToRad(90) },     // Left
 *   { action: walkBackward, radius: 1, azimuth: MathUtils.degToRad(180) }, // Backward
 *   { action: walkRight, radius: 1, azimuth: MathUtils.degToRad(270) },     // Right
 *
 *   // Run speed (radius = 2)
 *   { action: runForward, radius: 2, azimuth: MathUtils.degToRad(0) },     // Fast Forward
 *   { action: runLeft, radius: 2, azimuth: MathUtils.degToRad(90) },      // Fast Left
 *   { action: runBackward, radius: 2, azimuth: MathUtils.degToRad(180) },  // Fast Backward
 *   { action: runRight, radius: 2, azimuth: MathUtils.degToRad(270) },      // Fast Right
 * ], idle); // Center action for stationary state
 *
 * // Blend to medium speed northeast (45° at 1.5x speed)
 * blendTree.setBlend(1.5, MathUtils.degToRad(45));
 *
 * // Blend to slow walk forward
 * blendTree.setBlend(0.5, MathUtils.degToRad(0));
 * ```
 *
 * @public
 */
class PolarBlendTree extends AnimationTree {
    /**
     * Creates a new polar blend tree with the specified animation actions.
     * Actions are organized into rays (by azimuth) and rings (by radius) for
     * efficient bilinear interpolation. Initializes all actions to stopped state
     * and validates clip durations.
     *
     * @param polarActions - Array of polar actions defining the blend space.
     *                      Must contain at least 2 actions with unique coordinates.
     * @param centerAction - Optional center action at origin (0,0)
     * @throws {Error} When fewer than 2 actions are provided
     * @throws {Error} When any action has non-finite radius or azimuth values
     * @throws {Error} When any action has non-positive radius
     * @throws {Error} When any action has values outside JavaScript's safe range
     * @throws {Error} When multiple actions have the same polar coordinates
     * @throws {Error} When fewer than 2 rays are created (insufficient azimuth variety)
     * @throws {Error} When rays don't have consistent anchor counts for valid grid
     * @throws {Error} When any animation clip duration is not a positive finite number
     * @see {@link assertValidNumber} for coordinate validation details
     * @see {@link assertValidPositiveNumber} for radius and duration validation details
     */
    constructor(polarActions, centerAction) {
        super();
        this._private_tempAnchorMap = new Map();
        this._private_trackableAnchors = [];
        /** Array of rays (constant azimuth lines) containing anchors, sorted by azimuth */
        this._private_rays = [];
        /** Array of rings (constant radius circles) containing anchors, sorted by radius */
        this._private_rings = [];
        /** Current radial position for blending calculations */
        this._private_currentRadius = 0;
        /** Current angular position for blending calculations, normalized to [0, 2π) */
        this._private_currentAzimuth = 0;
        if (polarActions.length < MIN_POLAR_ACTIONS) {
            throw new Error(`At least ${MIN_POLAR_ACTIONS} actions are required for polar blend tree`);
        }
        for (let i = 0; i < polarActions.length; i++) {
            const { radius, azimuth } = polarActions[i];
            assertValidNumber(radius, `Polar action at index ${i} radius`);
            assertValidNumber(azimuth, `Polar action at index ${i} azimuth`);
            assertValidPositiveNumber(radius, `Polar action at index ${i} radius`);
        }
        for (const polarAction of polarActions) {
            polarAction.azimuth = calculateNormalizedAzimuth(polarAction.azimuth);
        }
        for (let i = 0; i < polarActions.length - 1; i++) {
            const azimuth = polarActions[i].azimuth;
            const radius = polarActions[i].radius;
            for (let j = i + 1; j < polarActions.length; j++) {
                if (Math.abs(azimuth - polarActions[j].azimuth) < EPSILON &&
                    Math.abs(radius - polarActions[j].radius) < EPSILON) {
                    throw new Error(`Duplicate coordinates found, azimuth: ${azimuth}, radius: ${radius}. All action values must be unique.`);
                }
            }
        }
        for (const polarAction of polarActions) {
            const animationAction = polarAction.action;
            const duration = animationAction.getClip().duration;
            assertValidPositiveNumber(duration, "Clip duration");
            animationAction.stop();
            animationAction.time = 0;
            animationAction.weight = 0;
            animationAction.paused = false;
            animationAction.enabled = false;
            const anchor = {
                index: getNextAnchorIndex(),
                action: animationAction,
                weight: 0,
                duration,
                invDuration: 1 / duration,
                iterationEventType: animationAction.loop === LoopOnce
                    ? AnimationStateEvent.FINISH
                    : AnimationStateEvent.ITERATE,
                radius: polarAction.radius,
                azimuth: polarAction.azimuth,
            };
            const ray = this._private_rays.find((r) => Math.abs(r.azimuth - polarAction.azimuth) < EPSILON);
            const ring = this._private_rings.find((r) => Math.abs(r.radius - polarAction.radius) < EPSILON);
            ray
                ? ray.anchors.push(anchor)
                : this._private_rays.push({ azimuth: anchor.azimuth, anchors: [anchor] });
            ring
                ? ring.anchors.push(anchor)
                : this._private_rings.push({ radius: anchor.radius, anchors: [anchor] });
            this.actionToAnchor.set(animationAction, anchor);
        }
        if (this._private_rays.length < 2) {
            throw new Error("At least two rays are required.");
        }
        const ringCount = this._private_rings.length;
        if (this._private_rays.some((r) => r.anchors.length !== ringCount)) {
            throw new Error("The anchors must form a valid grid.");
        }
        this._private_rays.sort((a, b) => a.azimuth - b.azimuth);
        for (const ray of this._private_rays) {
            ray.anchors.sort((a, b) => a.radius - b.radius);
        }
        this._private_rings.sort((a, b) => a.radius - b.radius);
        for (const ring of this._private_rings) {
            ring.anchors.sort((a, b) => a.azimuth - b.azimuth);
        }
        if (centerAction) {
            const duration = centerAction.getClip().duration;
            assertValidPositiveNumber(duration, "Center clip duration");
            centerAction.stop();
            centerAction.time = 0;
            centerAction.weight = 0;
            centerAction.paused = false;
            centerAction.enabled = false;
            this._private_centerAnchor = {
                index: getNextAnchorIndex(),
                action: centerAction,
                weight: 1,
                duration,
                invDuration: 1 / duration,
                iterationEventType: centerAction.loop === LoopOnce
                    ? AnimationStateEvent.FINISH
                    : AnimationStateEvent.ITERATE,
                get radius() {
                    throw new Error("PolarBlendTree central anchor radius is not accessible");
                },
                get azimuth() {
                    throw new Error("PolarBlendTree central anchor azimuth is not accessible");
                },
            };
            this._private_trackableAnchors.push(this._private_centerAnchor);
            this.actionToAnchor.set(centerAction, this._private_centerAnchor);
        }
        else {
            this._private_updateAnchors();
        }
    }
    get blendValue() {
        return { azimuth: this._private_currentAzimuth, radius: this._private_currentRadius };
    }
    /**
     * Sets the blend position in polar coordinates to determine animation weights.
     *
     * The azimuth is normalized to [0, 2π) range and radius must be non-negative.
     * When the position changes, animation weights are recalculated using bilinear
     * interpolation between the closest anchors.
     *
     * @param azimuth - Target angular position in radians (finite number). Will be normalized to [0, 2π).
     * @param radius - Target radial distance from origin (finite non-negative number).
     * @throws {Error} When azimuth is not a finite number
     * @throws {Error} When radius is not a finite non-negative number
     * @see {@link assertValidNumber} for azimuth validation details
     * @see {@link assertValidNonNegativeNumber} for radius validation details
     *
     * @example
     * ```typescript
     * // Blend to half speed, 45 degrees
     * blendTree.setBlend(0.5, MathUtils.degToRad(45));
     *
     * // Blend to full speed, straight back
     * blendTree.setBlend(1.0, MathUtils.degToRad(180));
     *
     * // Azimuth values are normalized to [0, 2π) range
     * blendTree.setBlend(1.0, MathUtils.degToRad(450)); // Becomes (1.0, ~1.57)
     * ```
     *
     * @public
     */
    setBlend(azimuth, radius) {
        assertValidNumber(azimuth, "Blend azimuth");
        assertValidNonNegativeNumber(radius, "Blend radius");
        const normalizedAzimuth = calculateNormalizedAzimuth(azimuth);
        if (this._private_currentRadius !== radius ||
            this._private_currentAzimuth !== normalizedAzimuth) {
            this._private_currentRadius = radius;
            this._private_currentAzimuth = normalizedAzimuth;
            this._private_updateAnchors();
        }
    }
    /**
     * Internal frame update method called by the animation state machine.
     *
     * Monitors animation progress for all active anchors and emits iteration events
     * when animations complete, restart, or loop. Tracks timing changes to detect
     * animation state transitions and prevent duplicate event emissions.
     *
     * @internal This method is called exclusively by the animation state machine
     * @override
     */
    ["onTickInternal"](deltaTime) {
        if (this.influence === 0) {
            throw new Error(`${this.name}: cannot update anchor time because the animation influence is zero`);
        }
        for (const anchor of this._private_trackableAnchors) {
            this.processTimeEvents(anchor, deltaTime);
            this.updateAnchorTime(anchor, deltaTime);
        }
    }
    ["onEnterInternal"]() {
        super.onEnterInternal();
        if (this.influence > 0) {
            for (const anchor of this._private_trackableAnchors) {
                this.resetFinishedAction(anchor);
            }
        }
    }
    /**
     * Updates the global influence for all active anchors in the polar blend tree.
     *
     * Called when the tree's overall influence changes but relative weights between
     * anchors should remain the same. Applies the current tree influence to all
     * active anchors while maintaining their existing weight distribution from
     * the polar blending calculations.
     *
     * @override
     * @protected
     */
    updateAnchorsInfluence() {
        for (const anchor of this._private_trackableAnchors) {
            this.updateAnchorWeight(anchor);
        }
    }
    /**
     * Recalculates and updates animation weights based on current blend position.
     *
     * This is the core blending algorithm that:
     * 1. Finds the two adjacent rays containing the current azimuth
     * 2. Calculates angular interpolation weights between these rays
     * 3. Determines if blending occurs in the center region or outer grid
     * 4. Applies appropriate interpolation (linear for center, bilinear for grid)
     * 5. Updates active anchors set and applies calculated weights
     *
     * The method handles three distinct cases:
     * - **Center Region**: When radius < first ring radius, blends with center action
     * - **Grid Region**: When radius >= first ring radius, uses bilinear interpolation
     * - **Edge Cases**: Boundary conditions and wraparound azimuth handling
     *
     * @private
     */
    _private_updateAnchors() {
        this._private_tempAnchorMap.clear();
        for (const anchor of this._private_trackableAnchors) {
            this._private_tempAnchorMap.set(anchor, 0);
        }
        for (let lRayIndex = 0; lRayIndex < this._private_rays.length; lRayIndex++) {
            const rRayIndex = (lRayIndex + 1) % this._private_rays.length;
            const lRay = this._private_rays[lRayIndex];
            const rRay = this._private_rays[rRayIndex];
            const lAzimuth = lRay.azimuth;
            const rAzimuth = rRay.azimuth;
            if (isAzimuthBetween(this._private_currentAzimuth, lAzimuth, rAzimuth)) {
                const angularDistance = calculateAngularDistanceForward(lAzimuth, rAzimuth);
                const leftDistance = calculateAngularDistanceForward(lAzimuth, this._private_currentAzimuth);
                const rRayT = leftDistance / angularDistance;
                const lRayT = 1 - rRayT;
                if (this._private_currentRadius <= this._private_rings[0].radius) {
                    let lWeight = lRayT;
                    let rWeight = rRayT;
                    if (this._private_centerAnchor) {
                        const ringWeight = this._private_currentRadius / this._private_rings[0].radius;
                        this._private_tempAnchorMap.set(this._private_centerAnchor, 1 - ringWeight);
                        lWeight *= ringWeight;
                        rWeight *= ringWeight;
                    }
                    this._private_tempAnchorMap.set(lRay.anchors[0], lWeight);
                    this._private_tempAnchorMap.set(rRay.anchors[0], rWeight);
                }
                else if (this._private_currentRadius >= this._private_rings[this._private_rings.length - 1].radius) {
                    let lWeight = lRayT;
                    let rWeight = rRayT;
                    this._private_tempAnchorMap.set(lRay.anchors[lRay.anchors.length - 1], lWeight);
                    this._private_tempAnchorMap.set(rRay.anchors[rRay.anchors.length - 1], rWeight);
                }
                else {
                    this._private_calculateBilinearWeights(this._private_tempAnchorMap, lRayT, rRayT, lRayIndex, rRayIndex);
                }
                break;
            }
        }
        this._private_trackableAnchors.length = 0;
        for (const [anchor, weight] of this._private_tempAnchorMap) {
            this.updateAnchorWeight(anchor, weight);
            if (weight > 0) {
                this._private_trackableAnchors.push(anchor);
            }
        }
    }
    /**
     * Calculates bilinear interpolation weights for the four corner anchors in the polar grid.
     *
     * This method performs standard bilinear interpolation between four points arranged
     * in a rectangular grid pattern in polar space. The four corners are defined by:
     * - Inner ring vs outer ring (radial dimension)
     * - Left ray vs right ray (angular dimension)
     *
     * The bilinear interpolation formula combines the radial and angular interpolation
     * weights to determine how much each of the four corner anchors contributes to
     * the final blend result.
     *
     * @param weights - Map to store calculated weights for each anchor
     * @param lRayT - Weight for the left ray (0 = all left, 1 = all right)
     * @param rRayT - Weight for the right ray (0 = all left, 1 = all right)
     * @param lRayIndex - Index of the left ray in the rays array
     * @param rRayIndex - Index of the right ray in the rays array
     *
     * @throws {Error} When no ring pair contains the current radius (should never happen)
     *
     * @private
     */
    _private_calculateBilinearWeights(weights, lRayT, rRayT, lRayIndex, rRayIndex) {
        for (let ringIndex = 0; ringIndex < this._private_rings.length - 1; ringIndex++) {
            const innerRing = this._private_rings[ringIndex];
            const outerRing = this._private_rings[ringIndex + 1];
            const innerRadius = innerRing.radius;
            const outerRadius = outerRing.radius;
            if (this._private_currentRadius >= innerRadius &&
                this._private_currentRadius <= outerRadius) {
                const outerRingT = (this._private_currentRadius - innerRadius) / (outerRadius - innerRadius);
                const innerRingT = 1 - outerRingT;
                weights.set(outerRing.anchors[lRayIndex], outerRingT * lRayT);
                weights.set(outerRing.anchors[rRayIndex], outerRingT * rRayT);
                weights.set(innerRing.anchors[lRayIndex], innerRingT * lRayT);
                weights.set(innerRing.anchors[rRayIndex], innerRingT * rRayT);
                return;
            }
        }
        // This code is unreachable under all inputs.
        // In a properly functioning system, currentRadius should always fall within the range of some ring pair.
        // This method is only called when currentRadius is between the first and last ring radii,
        // so there must always be at least one ring interval that contains the current radius.
        // If this branch is ever taken, it implies a violation of polar grid invariants and a bug elsewhere in the pipeline —
        // not an edge case to be covered by tests.
        /* c8 ignore next 3 */
        throw new Error(`Invariant violation: currentRadius ${this._private_currentRadius} not found within any ring interval`);
    }
}

export { AnimationMachine, AnimationState, AnimationStateEvent, ClipState, EPSILON, FreeformBlendTree, LinearBlendTree, PI2, PolarBlendTree, assertValidAzimuth, assertValidNonNegativeNumber, assertValidNumber, assertValidPositiveNumber, assertValidUnitRange, getNextAnchorIndex };
//# sourceMappingURL=index.js.map