import { AnimationAction, AnimationClip, AnimationMixer, normalizeQuat, slerpQuat, solveTwoBoneIk } from "../animation/browser-index.js";
import { invertMat4, multiplyMat4, Renderable, Scene, transformPoint } from "../scene/index.js";
export class GLTFSceneMorphTargetController {
    runtime;
    options;
    labels;
    labelToIndex = new Map();
    clampWeights;
    weights;
    lastApply;
    constructor(runtime, options) {
        this.runtime = runtime;
        this.options = options;
        if (options.target.trim().length === 0) {
            throw new Error("glTF morph target controller target cannot be empty.");
        }
        this.labels = [...(options.labels ?? [])];
        this.labels.forEach((label, index) => {
            if (label.trim().length === 0) {
                throw new Error("glTF morph target controller labels cannot be empty.");
            }
            this.labelToIndex.set(label, index);
        });
        this.clampWeights = options.clamp ?? true;
        this.weights = [...(options.initialWeights ?? new Array(Math.max(1, this.labels.length)).fill(0))].map((weight) => this.normalizeWeight(weight));
    }
    setWeight(indexOrLabel, weight) {
        const index = this.resolveIndex(indexOrLabel);
        this.ensureWeightIndex(index);
        this.weights[index] = this.normalizeWeight(weight);
        return this;
    }
    setWeights(weights) {
        if (weights.length === 0) {
            throw new Error("glTF morph target controller requires at least one morph weight.");
        }
        this.weights = weights.map((weight) => this.normalizeWeight(weight));
        return this;
    }
    getWeights() {
        return [...this.weights];
    }
    apply(time, label = `morph:${this.options.target}`) {
        this.lastApply = this.runtime.applyAnimationValues(label, time, new Map([[this.options.target, [...this.weights]]]));
        return this.lastApply;
    }
    snapshot() {
        return {
            target: this.options.target,
            labels: [...this.labels],
            weights: [...this.weights],
            ...(this.lastApply === undefined ? {} : { lastApply: this.lastApply })
        };
    }
    resolveIndex(indexOrLabel) {
        if (typeof indexOrLabel === "number") {
            if (!Number.isInteger(indexOrLabel) || indexOrLabel < 0) {
                throw new Error("glTF morph target controller index must be a non-negative integer.");
            }
            return indexOrLabel;
        }
        const index = this.labelToIndex.get(indexOrLabel);
        if (index === undefined) {
            throw new Error(`glTF morph target label "${indexOrLabel}" was not found.`);
        }
        return index;
    }
    ensureWeightIndex(index) {
        while (this.weights.length <= index) {
            this.weights.push(0);
        }
    }
    normalizeWeight(weight) {
        if (!Number.isFinite(weight)) {
            throw new Error("glTF morph target controller weights must be finite.");
        }
        if (!this.clampWeights)
            return weight;
        return Math.min(1, Math.max(0, weight));
    }
}
export class GLTFSceneAnimationCloneSampler {
    runtime;
    lastResults = [];
    constructor(runtime) {
        this.runtime = runtime;
    }
    sampleClones(samples, onSample) {
        const results = [];
        for (const sample of samples) {
            const applyResult = sample.weight === undefined && sample.additive !== true
                ? this.runtime.applyClipByName(sample.clipName, sample.time)
                : this.runtime.applyClips([{
                        clipName: sample.clipName,
                        time: sample.time,
                        ...(sample.weight === undefined ? {} : { weight: sample.weight }),
                        ...(sample.additive === undefined ? {} : { additive: sample.additive })
                    }]);
            const result = {
                cloneId: sample.cloneId,
                clipName: sample.clipName,
                time: applyResult.time,
                applyResult
            };
            results.push(result);
            onSample?.(result);
        }
        this.lastResults = results;
        return results;
    }
    snapshot() {
        return {
            cloneCount: new Set(this.lastResults.map((result) => result.cloneId)).size,
            lastSampleCount: this.lastResults.length,
            lastSkinningPalettesUpdated: this.lastResults.reduce((sum, result) => sum + result.applyResult.skinningPalettesUpdated, 0),
            lastResults: this.lastResults.map((result) => ({ ...result }))
        };
    }
}
export class GLTFImportedSkeletonIKController {
    runtime;
    options;
    target;
    pole;
    weight;
    allowStretch;
    apply;
    lastResult;
    constructor(runtime, options, initial) {
        this.runtime = runtime;
        this.options = options;
        this.target = initial.target;
        this.pole = initial.pole;
        this.weight = initial.weight;
        this.allowStretch = initial.allowStretch;
        this.apply = initial.apply ?? true;
    }
    setTarget(target) {
        this.target = target;
        return this;
    }
    setPole(pole) {
        this.pole = pole;
        return this;
    }
    setWeight(weight) {
        if (weight !== undefined && (!Number.isFinite(weight) || weight < 0 || weight > 1)) {
            throw new Error("glTF imported skeleton IK controller weight must be between 0 and 1.");
        }
        this.weight = weight;
        return this;
    }
    setAllowStretch(allowStretch) {
        this.allowStretch = allowStretch;
        return this;
    }
    setApply(apply) {
        this.apply = apply;
        return this;
    }
    solve(patch = {}) {
        if (patch.target !== undefined)
            this.setTarget(patch.target);
        if ("pole" in patch)
            this.setPole(patch.pole);
        if ("weight" in patch)
            this.setWeight(patch.weight);
        if ("allowStretch" in patch)
            this.setAllowStretch(patch.allowStretch);
        if (patch.apply !== undefined)
            this.setApply(patch.apply);
        this.lastResult = this.runtime.solveImportedSkeletonTwoBoneIK({
            ...this.options,
            target: this.target,
            ...(this.pole === undefined ? {} : { pole: this.pole }),
            ...(this.weight === undefined ? {} : { weight: this.weight }),
            ...(this.allowStretch === undefined ? {} : { allowStretch: this.allowStretch }),
            apply: this.apply
        });
        return this.lastResult;
    }
    snapshot() {
        return {
            target: this.target,
            ...(this.pole === undefined ? {} : { pole: this.pole }),
            ...(this.weight === undefined ? {} : { weight: this.weight }),
            ...(this.allowStretch === undefined ? {} : { allowStretch: this.allowStretch }),
            apply: this.apply,
            ...(this.options.skinName === undefined ? {} : { skinName: this.options.skinName }),
            ...(this.options.jointNames === undefined ? {} : { jointNames: this.options.jointNames }),
            ...(this.lastResult === undefined ? {} : { lastResult: this.lastResult })
        };
    }
}
export class GLTFSceneAnimationRuntime {
    options;
    clipsByName = new Map();
    nodesByName = new Map();
    morphRenderablesByNodeName = new Map();
    skinningBindings = [];
    lastApply;
    constructor(options) {
        this.options = options;
        for (const clip of options.clips) {
            this.clipsByName.set(clip.name, clip);
        }
        this.reindexScene();
    }
    applyClipByName(name, time) {
        const clip = this.clipsByName.get(name);
        if (!clip) {
            throw new Error(`glTF animation clip "${name}" was not found.`);
        }
        return this.applyClip(clip, time);
    }
    applyClip(clip, time) {
        if (!Number.isFinite(time) || time < 0) {
            throw new Error("glTF animation runtime time must be finite and non-negative.");
        }
        const wrappedTime = clip.duration > 0 && time > clip.duration ? time % clip.duration : Math.min(time, clip.duration);
        this.lastApply = this.applySampledTargets(clip.name, wrappedTime, sampleClipTracks(clip, wrappedTime, 1, false));
        return this.lastApply;
    }
    applyClips(samples) {
        if (samples.length === 0) {
            throw new Error("glTF animation runtime blend requires at least one clip sample.");
        }
        const accumulators = new Map();
        const unsupportedTracks = [];
        let maxTime = 0;
        const names = [];
        for (const sample of samples) {
            const clip = this.clipsByName.get(sample.clipName);
            if (!clip) {
                throw new Error(`glTF animation clip "${sample.clipName}" was not found.`);
            }
            if (!Number.isFinite(sample.time) || sample.time < 0) {
                throw new Error("glTF animation runtime blend time must be finite and non-negative.");
            }
            const weight = sample.weight ?? 1;
            if (!Number.isFinite(weight) || weight < 0) {
                throw new Error("glTF animation runtime blend weight must be finite and non-negative.");
            }
            if (weight === 0)
                continue;
            const wrappedTime = clip.duration > 0 && sample.time > clip.duration ? sample.time % clip.duration : Math.min(sample.time, clip.duration);
            maxTime = Math.max(maxTime, wrappedTime);
            names.push(`${clip.name}@${Number(wrappedTime.toFixed(4))}x${Number(weight.toFixed(4))}${sample.additive ? "+add" : ""}`);
            for (const track of clip.tracks) {
                const target = parseAnimationTarget(track.target);
                if (!target) {
                    unsupportedTracks.push(track.target);
                    continue;
                }
                blendInto(accumulators, track.target, track.valueType, track.sample(wrappedTime), weight, sample.additive === true);
            }
        }
        const sampledTargets = new Map();
        for (const [target, accumulator] of accumulators) {
            sampledTargets.set(target, finalizeTargetBlend(accumulator));
        }
        this.lastApply = this.applySampledTargets(`blend:${names.join(",")}`, maxTime, { sampledTargets, unsupportedTracks }, samples.length);
        return this.lastApply;
    }
    applyAnimationValues(label, time, values, unsupportedTracks = []) {
        if (label.trim().length === 0) {
            throw new Error("glTF animation runtime value application label cannot be empty.");
        }
        if (!Number.isFinite(time) || time < 0) {
            throw new Error("glTF animation runtime value application time must be finite and non-negative.");
        }
        this.lastApply = this.applySampledTargets(label, time, {
            sampledTargets: values,
            unsupportedTracks
        });
        return this.lastApply;
    }
    solveImportedSkeletonTwoBoneIK(options) {
        if (!this.options.asset || this.options.asset.skins.length === 0) {
            throw new Error("glTF imported skeleton IK requires an asset with at least one skin.");
        }
        const skin = resolveIKSkin(this.options.asset.skins, options.skinName);
        const jointNames = options.jointNames ?? firstTwoBoneJointChain(skin);
        const [rootName, midName, endName] = jointNames;
        this.options.scene.updateWorldTransforms();
        const rootNode = this.nodesByName.get(rootName)?.[0];
        const midNode = this.nodesByName.get(midName)?.[0];
        const endNode = this.nodesByName.get(endName)?.[0];
        const missingTargets = [
            ...(!rootNode ? [`${skin.name}.${rootName}`] : []),
            ...(!midNode ? [`${skin.name}.${midName}`] : []),
            ...(!endNode ? [`${skin.name}.${endName}`] : [])
        ];
        if (!rootNode || !midNode || !endNode) {
            return {
                skinName: skin.name,
                jointNames,
                solution: solveTwoBoneIk({
                    root: [0, 0, 0],
                    mid: [0, 1, 0],
                    end: [0, 2, 0],
                    target: [0, 2, 0.01],
                    allowStretch: true
                }),
                applied: false,
                skinningPalettesUpdated: 0,
                missingTargets
            };
        }
        const solution = solveTwoBoneIk({
            root: worldPosition(rootNode),
            mid: worldPosition(midNode),
            end: worldPosition(endNode),
            target: options.target,
            ...(options.pole ? { pole: options.pole } : {}),
            ...(options.weight !== undefined ? { weight: options.weight } : {}),
            ...(options.allowStretch !== undefined ? { allowStretch: options.allowStretch } : {})
        });
        if (options.apply === false) {
            return {
                skinName: skin.name,
                jointNames,
                solution,
                applied: false,
                skinningPalettesUpdated: 0,
                missingTargets: []
            };
        }
        setWorldPosition(midNode, solution.mid);
        this.options.scene.updateWorldTransforms();
        setWorldPosition(endNode, solution.end);
        this.options.scene.updateWorldTransforms();
        const skinning = this.refreshSkinningPalettes();
        return {
            skinName: skin.name,
            jointNames,
            solution,
            applied: true,
            skinningPalettesUpdated: skinning.updated,
            missingTargets: skinning.missingTargets
        };
    }
    createTwoBoneIKController(options) {
        return new GLTFImportedSkeletonIKController(this, {
            ...(options.skinName === undefined ? {} : { skinName: options.skinName }),
            ...(options.jointNames === undefined ? {} : { jointNames: options.jointNames })
        }, {
            target: options.target,
            ...(options.pole === undefined ? {} : { pole: options.pole }),
            ...(options.weight === undefined ? {} : { weight: options.weight }),
            ...(options.allowStretch === undefined ? {} : { allowStretch: options.allowStretch }),
            ...(options.apply === undefined ? {} : { apply: options.apply })
        });
    }
    createCloneSampler() {
        return new GLTFSceneAnimationCloneSampler(this);
    }
    createMorphTargetController(options) {
        return new GLTFSceneMorphTargetController(this, options);
    }
    snapshot() {
        return {
            clipCount: this.clipsByName.size,
            nodeTargetCount: this.nodesByName.size,
            morphTargetNodeCount: this.morphRenderablesByNodeName.size,
            skinningBindingCount: this.skinningBindings.length,
            clips: [...this.clipsByName.keys()],
            ...(this.lastApply ? { lastApply: this.lastApply } : {})
        };
    }
    inspectClipBindings(name) {
        if (name !== undefined) {
            const clip = this.clipsByName.get(name);
            if (!clip) {
                throw new Error(`glTF animation clip "${name}" was not found.`);
            }
            return [this.inspectClipBinding(clip)];
        }
        return [...this.clipsByName.values()].map((clip) => this.inspectClipBinding(clip));
    }
    reindexScene() {
        this.nodesByName.clear();
        this.morphRenderablesByNodeName.clear();
        this.skinningBindings.length = 0;
        this.options.scene.traverse((node) => {
            const nodes = this.nodesByName.get(node.name) ?? [];
            nodes.push(node);
            this.nodesByName.set(node.name, nodes);
        });
        for (const { node, renderable } of this.options.scene.collectRenderables()) {
            if (renderable.morphWeights.length === 0)
                continue;
            for (const nodeName of new Set([node.name, node.parent?.name].filter((name) => typeof name === "string" && name.length > 0))) {
                const renderables = this.morphRenderablesByNodeName.get(nodeName) ?? [];
                renderables.push(renderable);
                this.morphRenderablesByNodeName.set(nodeName, renderables);
            }
        }
        if (this.options.asset) {
            const meshesByName = new Map(this.options.asset.meshes.map((mesh) => [mesh.name, mesh]));
            for (const { node, renderable } of this.options.scene.collectRenderables()) {
                if (!renderable.skinning)
                    continue;
                const mesh = meshesByName.get(renderable.geometry);
                const skin = mesh?.skinIndex === undefined ? undefined : this.options.asset.skins[mesh.skinIndex];
                if (!mesh || !skin || skin.joints.length > 64)
                    continue;
                this.skinningBindings.push({ node, renderable, mesh, skin });
            }
        }
    }
    refreshSkinningPalettes() {
        if (this.skinningBindings.length === 0) {
            return { updated: 0, missingTargets: [] };
        }
        let updated = 0;
        const missingTargets = [];
        for (const binding of this.skinningBindings) {
            const matrices = new Float32Array(binding.skin.joints.length * 16);
            const inverseMeshWorld = invertMat4(binding.node.transform.worldMatrix);
            let complete = true;
            for (let index = 0; index < binding.skin.jointNames.length; index += 1) {
                const jointName = binding.skin.jointNames[index];
                const jointNode = this.nodesByName.get(jointName)?.[0];
                const inverseBind = binding.skin.inverseBindMatrices[index];
                if (!jointNode || !inverseBind) {
                    missingTargets.push(`${binding.skin.name}.${jointName}`);
                    complete = false;
                    break;
                }
                const jointMatrix = multiplyMat4(multiplyMat4(inverseMeshWorld, jointNode.transform.worldMatrix), inverseBind);
                matrices.set(jointMatrix, index * 16);
            }
            if (!complete)
                continue;
            binding.renderable.skinning = {
                jointCount: binding.skin.joints.length,
                matrices
            };
            updated += 1;
        }
        return { updated, missingTargets };
    }
    inspectClipBinding(clip) {
        let supportedTrackCount = 0;
        let boundTrackCount = 0;
        let transformTrackCount = 0;
        let morphWeightTrackCount = 0;
        const boundNodeNames = new Set();
        const missingTargets = new Set();
        const unsupportedTracks = new Set();
        for (const track of clip.tracks) {
            const target = parseAnimationTarget(track.target);
            if (!target) {
                unsupportedTracks.add(track.target);
                continue;
            }
            supportedTrackCount += 1;
            if (target.path === "weights") {
                morphWeightTrackCount += 1;
                const renderables = this.morphRenderablesByNodeName.get(target.nodeName) ?? [];
                if (renderables.length === 0) {
                    missingTargets.add(track.target);
                    continue;
                }
                boundTrackCount += 1;
                boundNodeNames.add(target.nodeName);
                continue;
            }
            transformTrackCount += 1;
            const nodes = this.nodesByName.get(target.nodeName) ?? [];
            if (nodes.length === 0) {
                missingTargets.add(track.target);
                continue;
            }
            boundTrackCount += 1;
            boundNodeNames.add(target.nodeName);
        }
        return {
            clipName: clip.name,
            trackCount: clip.tracks.length,
            supportedTrackCount,
            boundTrackCount,
            transformTrackCount,
            morphWeightTrackCount,
            missingTargetCount: missingTargets.size,
            unsupportedTrackCount: unsupportedTracks.size,
            skinningBindingCount: this.skinningBindings.length,
            boundNodeNames: [...boundNodeNames].sort(),
            missingTargets: [...missingTargets].sort(),
            unsupportedTracks: [...unsupportedTracks].sort(),
            animatesSkeleton: this.skinningBindings.length > 0 && boundTrackCount > 0 && transformTrackCount > 0
        };
    }
    applySampledTargets(clipName, time, sampled, blendedClipCount) {
        let transformTracksApplied = 0;
        let morphWeightTracksApplied = 0;
        const missingTargets = [];
        for (const [trackTarget, value] of sampled.sampledTargets) {
            const target = parseAnimationTarget(trackTarget);
            if (!target) {
                continue;
            }
            if (target.path === "weights") {
                const renderables = this.morphRenderablesByNodeName.get(target.nodeName) ?? [];
                if (renderables.length === 0) {
                    missingTargets.push(trackTarget);
                    continue;
                }
                const weights = asNumberArray(value);
                for (const renderable of renderables) {
                    renderable.morphWeights = weights;
                }
                morphWeightTracksApplied += 1;
                continue;
            }
            const nodes = this.nodesByName.get(target.nodeName) ?? [];
            if (nodes.length === 0) {
                missingTargets.push(trackTarget);
                continue;
            }
            for (const node of nodes) {
                applyTransformValue(node, target.path, value, trackTarget);
            }
            transformTracksApplied += 1;
        }
        this.options.scene.updateWorldTransforms();
        const skinning = this.refreshSkinningPalettes();
        return {
            clipName,
            time,
            ...(blendedClipCount === undefined ? {} : { blendedClipCount }),
            tracksApplied: transformTracksApplied + morphWeightTracksApplied,
            transformTracksApplied,
            morphWeightTracksApplied,
            skinningPalettesUpdated: skinning.updated,
            missingTargets: [...missingTargets, ...skinning.missingTargets],
            unsupportedTracks: sampled.unsupportedTracks
        };
    }
}
export function createGLTFSceneAnimationRuntime(options) {
    return new GLTFSceneAnimationRuntime(options);
}
export class GLTFSceneAnimationMixerBinding {
    runtime;
    mixer;
    actions;
    pendingValues = new Map();
    elapsedTime = 0;
    lastApply;
    constructor(options) {
        this.runtime = new GLTFSceneAnimationRuntime(options);
        this.mixer = new AnimationMixer({
            setAnimationValue: (target, value) => {
                this.pendingValues.set(target, cloneAnimationValue(value));
            }
        }, options.mixer ?? {});
        const actions = new Map();
        for (const clip of options.clips) {
            const action = new AnimationAction(clip).setWeight(0);
            this.mixer.addAction(action);
            actions.set(clip.name, action);
        }
        const autoPlay = options.autoPlay === false ? undefined : options.autoPlay ?? options.clips[0]?.name;
        if (autoPlay) {
            const action = actions.get(autoPlay);
            if (!action) {
                throw new Error(`glTF animation mixer autoPlay clip "${autoPlay}" was not found.`);
            }
            action.setWeight(1).play();
        }
        this.actions = actions;
    }
    listClips() {
        return [...this.actions.keys()];
    }
    play(name, weight = 1) {
        return this.playClip(name, { weight });
    }
    playClip(name, options = {}) {
        const action = this.requireAction(name);
        this.configureAction(action, options);
        action.play();
        return action;
    }
    playExclusive(name, options = {}) {
        const action = this.requireAction(name);
        const fadeDuration = options.fadeDuration ?? 0;
        for (const [clipName, candidate] of this.actions) {
            if (clipName === name)
                continue;
            if (fadeDuration > 0) {
                candidate.fadeTo(0, fadeDuration);
            }
            else {
                candidate.setWeight(0);
            }
        }
        this.configureAction(action, { ...options, weight: options.weight ?? 1 });
        action.play();
        return action;
    }
    pause(name) {
        for (const action of this.resolveActions(name)) {
            action.pause();
        }
    }
    resume(name) {
        for (const action of this.resolveActions(name)) {
            if (action.weight > 0)
                action.play();
        }
    }
    stop(name) {
        for (const action of this.resolveActions(name)) {
            action.stop().setWeight(0);
        }
    }
    seek(name, time) {
        const action = this.requireAction(name);
        action.seek(time);
        this.elapsedTime = action.time;
        return action;
    }
    setTimeScale(timeScale) {
        if (!Number.isFinite(timeScale) || timeScale < 0) {
            throw new Error("glTF animation mixer timeScale must be finite and non-negative.");
        }
        this.mixer.timeScale = timeScale;
    }
    setActionTimeScale(name, timeScale) {
        return this.requireAction(name).setTimeScale(timeScale);
    }
    setActionWeight(name, weight) {
        return this.requireAction(name).setWeight(weight);
    }
    setActionLoop(name, loopMode) {
        return this.requireAction(name).setLoop(loopMode);
    }
    crossFade(fromName, toName, duration) {
        const from = this.requireAction(fromName);
        const to = this.requireAction(toName);
        if (!to.playing) {
            to.setWeight(0).play();
        }
        this.mixer.crossFade(from, to, duration);
    }
    applyClipSamples(samples) {
        this.pendingValues.clear();
        this.lastApply = this.runtime.applyClips(samples);
        this.elapsedTime = Math.max(this.elapsedTime, this.lastApply.time);
        return { events: [], applyResult: this.lastApply, activeActions: this.actionSnapshots().filter((action) => action.active) };
    }
    update(deltaSeconds) {
        if (!Number.isFinite(deltaSeconds) || deltaSeconds < 0) {
            throw new Error("glTF animation mixer delta must be finite and non-negative.");
        }
        this.pendingValues.clear();
        const events = this.mixer.update(deltaSeconds);
        this.elapsedTime += deltaSeconds;
        this.lastApply = this.runtime.applyAnimationValues("gltf-animation-mixer", this.elapsedTime, this.pendingValues);
        return { events, applyResult: this.lastApply, activeActions: this.actionSnapshots().filter((action) => action.active) };
    }
    getAction(name) {
        return this.actions.get(name);
    }
    snapshot() {
        return {
            ...this.runtime.snapshot(),
            mixerActionCount: this.actions.size,
            pendingValueCount: this.pendingValues.size,
            elapsedTime: this.elapsedTime,
            timeScale: this.mixer.timeScale,
            activeClipNames: this.actionSnapshots().filter((action) => action.active).map((action) => action.clipName),
            actions: this.actionSnapshots(),
            ...(this.lastApply ? { lastApply: this.lastApply } : {})
        };
    }
    configureAction(action, options) {
        if (options.reset === true) {
            action.reset();
        }
        if (options.weight !== undefined) {
            if (options.fadeDuration !== undefined && options.fadeDuration > 0) {
                action.fadeTo(options.weight, options.fadeDuration);
            }
            else {
                action.setWeight(options.weight);
            }
        }
        if (options.timeScale !== undefined) {
            action.setTimeScale(options.timeScale);
        }
        if (options.loopMode !== undefined) {
            action.setLoop(options.loopMode);
        }
    }
    resolveActions(name) {
        return name === undefined ? [...this.actions.values()] : [this.requireAction(name)];
    }
    actionSnapshots() {
        return [...this.actions.values()].map((action) => {
            const snapshot = action.snapshot();
            return {
                ...snapshot,
                active: snapshot.playing && !snapshot.paused && snapshot.weight > 0
            };
        });
    }
    requireAction(name) {
        const action = this.actions.get(name);
        if (!action) {
            throw new Error(`glTF animation action "${name}" was not found.`);
        }
        return action;
    }
}
export function createGLTFSceneAnimationMixer(options) {
    return new GLTFSceneAnimationMixerBinding(options);
}
function parseAnimationTarget(target) {
    const separator = target.lastIndexOf(".");
    if (separator <= 0 || separator === target.length - 1) {
        return undefined;
    }
    const path = target.slice(separator + 1);
    if (path !== "translation" && path !== "rotation" && path !== "scale" && path !== "weights") {
        return undefined;
    }
    return {
        nodeName: target.slice(0, separator),
        path
    };
}
function resolveIKSkin(skins, skinName) {
    if (skinName === undefined) {
        const skin = skins.find((candidate) => candidate.jointNames.length >= 3);
        if (!skin) {
            throw new Error("glTF imported skeleton IK requires a skin with at least three joints.");
        }
        return skin;
    }
    const skin = skins.find((candidate) => candidate.name === skinName);
    if (!skin) {
        throw new Error(`glTF imported skeleton IK skin "${skinName}" was not found.`);
    }
    if (skin.jointNames.length < 3) {
        throw new Error(`glTF imported skeleton IK skin "${skinName}" does not contain a two-bone chain.`);
    }
    return skin;
}
function firstTwoBoneJointChain(skin) {
    const root = skin.jointNames[0];
    const mid = skin.jointNames[1];
    const end = skin.jointNames[2];
    if (!root || !mid || !end) {
        throw new Error(`glTF imported skeleton IK skin "${skin.name}" does not contain a two-bone chain.`);
    }
    return [root, mid, end];
}
function worldPosition(node) {
    node.updateWorldTransform();
    return [node.transform.worldMatrix[12], node.transform.worldMatrix[13], node.transform.worldMatrix[14]];
}
function setWorldPosition(node, position) {
    const local = node.parent
        ? transformPoint(invertMat4(node.parent.transform.worldMatrix), [position[0], position[1], position[2]])
        : [position[0], position[1], position[2]];
    node.transform.setPosition(local[0], local[1], local[2]);
}
function applyTransformValue(node, path, value, target) {
    if (path === "translation") {
        const vector = asVec3(value, target);
        node.transform.setPosition(vector[0], vector[1], vector[2]);
        return;
    }
    if (path === "rotation") {
        const quat = asQuat(value, target);
        node.transform.setRotation(quat[0], quat[1], quat[2], quat[3]);
        return;
    }
    const vector = asVec3(value, target);
    node.transform.setScale(vector[0], vector[1], vector[2]);
}
function asVec3(value, target) {
    if (!Array.isArray(value) || value.length !== 3 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
        throw new Error(`glTF animation track "${target}" sampled an invalid vec3.`);
    }
    return [value[0], value[1], value[2]];
}
function asQuat(value, target) {
    if (!Array.isArray(value) || value.length !== 4 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
        throw new Error(`glTF animation track "${target}" sampled an invalid quaternion.`);
    }
    return [value[0], value[1], value[2], value[3]];
}
function asNumberArray(value) {
    if (!Array.isArray(value) || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
        throw new Error("glTF morph-weight animation sampled an invalid number array.");
    }
    return [...value];
}
function sampleClipTracks(clip, time, weight, additive) {
    const accumulators = new Map();
    const unsupportedTracks = [];
    for (const track of clip.tracks) {
        if (!parseAnimationTarget(track.target)) {
            unsupportedTracks.push(track.target);
            continue;
        }
        blendInto(accumulators, track.target, track.valueType, track.sample(time), weight, additive);
    }
    const sampledTargets = new Map();
    for (const [target, accumulator] of accumulators) {
        sampledTargets.set(target, finalizeTargetBlend(accumulator));
    }
    return { sampledTargets, unsupportedTracks };
}
function blendInto(accumulators, target, type, value, weight, additive) {
    if (weight <= 0)
        return;
    const current = accumulators.get(target);
    if (!current) {
        const accumulator = { type };
        if (additive) {
            accumulator.additive = additiveContribution(type, value, weight);
        }
        else {
            accumulator.base = { value: cloneAnimationValue(value), weight, type };
        }
        accumulators.set(target, accumulator);
        return;
    }
    if (current.type !== type) {
        throw new Error(`Cannot blend ${type} glTF animation track into existing ${current.type} target ${target}.`);
    }
    if (additive) {
        current.additive = current.additive === undefined
            ? additiveContribution(type, value, weight)
            : combineAdditive(type, current.additive, value, weight);
        return;
    }
    if (!current.base) {
        current.base = { value: cloneAnimationValue(value), weight, type };
        return;
    }
    blendBase(current.base, type, value, weight);
}
function blendBase(current, type, value, weight) {
    const total = current.weight + weight;
    const t = weight / total;
    if (type === "scalar") {
        current.value = current.value + (value - current.value) * t;
    }
    else if (type === "vector3") {
        const a = current.value;
        const b = value;
        current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
    }
    else if (type === "quaternion") {
        current.value = slerpQuat(current.value, value, t);
    }
    else if (type === "number-array") {
        const a = current.value;
        const b = value;
        if (a.length !== b.length) {
            throw new Error("Cannot blend glTF morph animation values with different lengths.");
        }
        current.value = a.map((component, index) => component + (b[index] - component) * t);
    }
    else {
        current.value = cloneAnimationValue(value);
    }
    current.weight = total;
}
function finalizeTargetBlend(accumulator) {
    const base = accumulator.base
        ? finalizeBaseBlend(accumulator.base)
        : accumulator.additive !== undefined
            ? additiveNeutral(accumulator.type, accumulator.additive)
            : undefined;
    if (base === undefined) {
        throw new Error("glTF animation target accumulator has no sampled value.");
    }
    return accumulator.additive === undefined ? base : applyAdditive(accumulator.type, base, accumulator.additive);
}
function finalizeBaseBlend(accumulator) {
    return accumulator.type === "quaternion"
        ? normalizeQuat(accumulator.value)
        : cloneAnimationValue(accumulator.value);
}
function additiveContribution(type, value, weight) {
    if (type === "scalar")
        return value * weight;
    if (type === "vector3") {
        const vector = value;
        return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
    }
    if (type === "number-array") {
        return value.map((component) => component * weight);
    }
    if (type === "quaternion") {
        return slerpQuat([0, 0, 0, 1], normalizeQuat(value), weight);
    }
    throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
}
function combineAdditive(type, current, value, weight) {
    return applyAdditive(type, current, additiveContribution(type, value, weight));
}
function applyAdditive(type, base, delta) {
    if (type === "scalar")
        return base + delta;
    if (type === "vector3") {
        const a = base;
        const b = delta;
        return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
    }
    if (type === "number-array") {
        const a = base;
        const b = delta;
        if (a.length !== b.length) {
            throw new Error("Cannot add glTF morph animation values with different lengths.");
        }
        return a.map((component, index) => component + b[index]);
    }
    if (type === "quaternion") {
        return normalizeQuat(multiplyQuat(base, delta));
    }
    throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
}
function additiveNeutral(type, value) {
    if (type === "scalar")
        return 0;
    if (type === "vector3")
        return [0, 0, 0];
    if (type === "number-array")
        return value.map(() => 0);
    if (type === "quaternion")
        return [0, 0, 0, 1];
    throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
}
function cloneAnimationValue(value) {
    return Array.isArray(value) ? [...value] : value;
}
function multiplyQuat(a, b) {
    return [
        a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
        a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
        a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
        a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
    ];
}
//# sourceMappingURL=GLTFAnimationRuntime.js.map