import { Box3, Box3Helper, Color, Euler, GridHelper, Mesh, MeshStandardMaterial, OrthographicCamera, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, ShadowMaterial, Texture, Uniform, Vector3 } from "three";
import { ShaderMaterial, WebGLRenderer } from "three";
import { GroundedSkybox } from "three/examples/jsm/objects/GroundedSkybox.js";
import { useForAutoFit } from "./engine_camera.js";
import { Mathf } from "./engine_math.js";
import { CircularBuffer } from "./engine_utils.js";
/**
 * Slerp between two vectors
 */
export function slerp(vec, end, t) {
    const len1 = vec.length();
    const len2 = end.length();
    const targetLen = Mathf.lerp(len1, len2, t);
    return vec.lerp(end, t).normalize().multiplyScalar(targetLen);
}
const _tempQuat = new Quaternion();
const flipYQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
export function lookAtInverse(obj, target) {
    obj.lookAt(target);
    obj.quaternion.multiply(flipYQuat);
}
/** Better lookAt
 * @param object the object that the lookAt should be applied to
 * @param target the target to look at
 * @param keepUpDirection if true the up direction will be kept
 * @param copyTargetRotation if true the target rotation will be copied so the rotation is not skewed
 */
export function lookAtObject(object, target, keepUpDirection = true, copyTargetRotation = false) {
    if (object === target)
        return;
    _tempQuat.copy(object.quaternion);
    const lookTarget = getWorldPosition(target);
    const lookFrom = getWorldPosition(object);
    if (copyTargetRotation) {
        setWorldQuaternion(object, getWorldQuaternion(target));
        // look at forward again so we don't get any roll
        if (keepUpDirection) {
            const ypos = lookFrom.y;
            const forwardPoint = lookFrom.sub(getWorldDirection(object));
            forwardPoint.y = ypos;
            object.lookAt(forwardPoint);
            object.quaternion.multiply(flipYQuat);
        }
        // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled
        if (Number.isNaN(object.quaternion.x)) {
            object.quaternion.copy(_tempQuat);
        }
        return;
    }
    if (keepUpDirection) {
        lookTarget.y = lookFrom.y;
    }
    object.lookAt(lookTarget);
    // sanitize – three.js sometimes returns NaN values when parents are non-uniformly scaled
    if (Number.isNaN(object.quaternion.x)) {
        object.quaternion.copy(_tempQuat);
    }
}
/**
 * Look at a 2D point in screen space
 * @param object the object to look at the point
 * @param target the target point in 2D screen space XY e.g. from a mouse event
 * @param camera the camera to use for the lookAt
 * @param factor the factor to multiply the distance from the camera to the object. Default is 1
 * @returns the target point in world space
 *
 * @example Needle Engine Component
 * ```ts
 * export class MyLookAtComponent extends Behaviour {
 *   update() {
 *     lookAtScreenPoint(this.gameObject, this.context.input.mousePosition, this.context.mainCamera);
 *   }
 * }
 * ```
 *
 * @example Look at from browser mouse move event
 * ```ts
 * window.addEventListener("mousemove", (e) => {
 *   lookAtScreenPoint(object, new Vector2(e.clientX, e.clientY), camera);
 *  });
 * ```
 */
export function lookAtScreenPoint(object, target, camera, factor = 1) {
    if (camera) {
        const pos = getTempVector(0, 0, 0);
        const ndcx = (target.x / window.innerWidth) * 2 - 1;
        const ndcy = -(target.y / window.innerHeight) * 2 + 1;
        pos.set(ndcx, ndcy, 0);
        pos.unproject(camera);
        // get distance from object to camera
        const camPos = camera.worldPosition;
        const dist = object.worldPosition.distanceTo(camPos);
        // Create direction from camera through cursor point
        const dir = pos.sub(camPos);
        dir.multiplyScalar(factor * 3.6 * dist);
        const targetPoint = camera.worldPosition.add(dir);
        object.lookAt(targetPoint);
        return targetPoint;
    }
    return null;
}
const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
export function getTempVector(vecOrX, y, z) {
    const vec = _tempVecs.get();
    vec.set(0, 0, 0); // initialize with default values
    if (vecOrX instanceof Vector3)
        vec.copy(vecOrX);
    else if (Array.isArray(vecOrX))
        vec.set(vecOrX[0], vecOrX[1], vecOrX[2]);
    else if (vecOrX instanceof DOMPointReadOnly)
        vec.set(vecOrX.x, vecOrX.y, vecOrX.z);
    else {
        if (typeof vecOrX === "number") {
            vec.x = vecOrX;
            vec.y = y !== undefined ? y : vec.x;
            vec.z = z !== undefined ? z : vec.x;
        }
        else if (typeof vecOrX === "object") {
            vec.x = vecOrX.x;
            vec.y = vecOrX.y;
            vec.z = vecOrX.z;
        }
    }
    return vec;
}
const _tempColors = new CircularBuffer(() => new Color(), 30);
export function getTempColor(color) {
    const col = _tempColors.get();
    if (color)
        col.copy(color);
    else {
        col.set(0, 0, 0);
    }
    return col;
}
const _tempQuats = new CircularBuffer(() => new Quaternion(), 100);
export function getTempQuaternion(value, y, z, w) {
    const val = _tempQuats.get();
    val.identity();
    if (value instanceof Quaternion)
        val.copy(value);
    else if (value instanceof DOMPointReadOnly)
        val.set(value.x, value.y, value.z, value.w);
    else {
        if (typeof value === "number" && y !== undefined && z !== undefined && w !== undefined) {
            val.set(value, y, z, w);
        }
        else if (typeof value === "object" && 'x' in value && 'y' in value && 'z' in value && 'w' in value) {
            val.set(value.x, value.y, value.z, value.w);
        }
    }
    return val;
}
const _worldPositions = new CircularBuffer(() => new Vector3(), 100);
const _lastMatrixWorldUpdateKey = Symbol("lastMatrixWorldUpdateKey");
/**
 * Get the world position of an object
 * @param obj the object to get the world position from
 * @param vec a vector to store the result in. If not passed in a temporary vector will be used
 * @param updateParents if true the parents will be updated before getting the world position
 * @returns the world position
 */
export function getWorldPosition(obj, vec = null, updateParents = true) {
    const wp = vec ?? _worldPositions.get();
    if (!obj)
        return wp.set(0, 0, 0);
    if (!obj.parent)
        return wp.copy(obj.position);
    if (updateParents)
        obj.updateWorldMatrix(true, false);
    if (obj.matrixWorldNeedsUpdate && obj[_lastMatrixWorldUpdateKey] !== Date.now()) {
        obj[_lastMatrixWorldUpdateKey] = Date.now();
        obj.updateMatrixWorld();
    }
    wp.setFromMatrixPosition(obj.matrixWorld);
    return wp;
}
/**
 * Set the world position of an object
 * @param obj the object to set the world position of
 * @param val the world position to set
 */
export function setWorldPosition(obj, val) {
    if (!obj)
        return obj;
    const wp = _worldPositions.get();
    if (val !== wp)
        wp.copy(val);
    if (obj.parent !== null)
        obj.parent.worldToLocal(wp);
    obj.position.set(wp.x, wp.y, wp.z);
    return obj;
}
/**
 * Set the world position of an object
 * @param obj the object to set the world position of
 * @param x the x position
 * @param y the y position
 * @param z the z position
 */
export function setWorldPositionXYZ(obj, x, y, z) {
    const wp = _worldPositions.get();
    wp.set(x, y, z);
    setWorldPosition(obj, wp);
    return obj;
}
const _worldQuaternions = new CircularBuffer(() => new Quaternion(), 100);
const _worldQuaternionBuffer = new Quaternion();
const _tempQuaternionBuffer2 = new Quaternion();
export function getWorldQuaternion(obj, target = null) {
    if (!obj)
        return _worldQuaternions.get().identity();
    const quat = target ?? _worldQuaternions.get();
    if (!obj.parent)
        return quat.copy(obj.quaternion);
    obj.getWorldQuaternion(quat);
    return quat;
}
export function setWorldQuaternion(obj, val) {
    if (!obj)
        return;
    if (val !== _worldQuaternionBuffer)
        _worldQuaternionBuffer.copy(val);
    const tempVec = _worldQuaternionBuffer;
    const parent = obj?.parent;
    parent?.getWorldQuaternion(_tempQuaternionBuffer2);
    _tempQuaternionBuffer2.invert();
    const q = _tempQuaternionBuffer2.multiply(tempVec);
    // console.log(tempVec);
    obj.quaternion.set(q.x, q.y, q.z, q.w);
    // console.error("quaternion world to local is not yet implemented");
}
export function setWorldQuaternionXYZW(obj, x, y, z, w) {
    _worldQuaternionBuffer.set(x, y, z, w);
    setWorldQuaternion(obj, _worldQuaternionBuffer);
}
const _worldScaleBuffer = new CircularBuffer(() => new Vector3(), 100);
const _worldScale = new Vector3();
export function getWorldScale(obj, vec = null) {
    if (!vec)
        vec = _worldScaleBuffer.get();
    if (!obj)
        return vec.set(0, 0, 0);
    if (!obj.parent)
        return vec.copy(obj.scale);
    obj.getWorldScale(vec);
    return vec;
}
export function setWorldScale(obj, vec) {
    if (!obj)
        return;
    if (!obj.parent) {
        obj.scale.copy(vec);
        return;
    }
    const tempVec = _worldScale;
    const obj2 = obj.parent;
    obj2.getWorldScale(tempVec);
    obj.scale.copy(vec);
    obj.scale.divide(tempVec);
}
const _forward = new Vector3();
const _forwardQuat = new Quaternion();
export function forward(obj) {
    getWorldQuaternion(obj, _forwardQuat);
    return _forward.set(0, 0, 1).applyQuaternion(_forwardQuat);
}
const _worldDirectionBuffer = new CircularBuffer(() => new Vector3(), 100);
const _worldDirectionQuat = new Quaternion();
/** Get the world direction. Returns world forward if nothing is passed in.
 * Pass in a relative direction to get it converted to world space (e.g. dir = new Vector3(0, 1, 1))
 * The returned vector will not be normalized
 */
export function getWorldDirection(obj, dir) {
    // If no direction is passed in set the direction to the forward vector
    if (!dir)
        dir = _worldDirectionBuffer.get().set(0, 0, 1);
    getWorldQuaternion(obj, _worldDirectionQuat);
    return dir.applyQuaternion(_worldDirectionQuat);
}
const _worldEulerBuffer = new Euler();
const _worldEuler = new Euler();
const _worldRotation = new Vector3();
// world euler (in radians)
export function getWorldEuler(obj) {
    const quat = _worldQuaternions.get();
    obj.getWorldQuaternion(quat);
    _worldEuler.setFromQuaternion(quat);
    return _worldEuler;
}
// world euler (in radians)
export function setWorldEuler(obj, val) {
    const quat = _worldQuaternions.get();
    setWorldQuaternion(obj, quat.setFromEuler(val));
    ;
}
// returns rotation in degrees
export function getWorldRotation(obj) {
    const rot = getWorldEuler(obj);
    const wr = _worldRotation;
    wr.set(rot.x, rot.y, rot.z);
    wr.x = Mathf.toDegrees(wr.x);
    wr.y = Mathf.toDegrees(wr.y);
    wr.z = Mathf.toDegrees(wr.z);
    return wr;
}
export function setWorldRotation(obj, val) {
    setWorldRotationXYZ(obj, val.x, val.y, val.z, true);
}
export function setWorldRotationXYZ(obj, x, y, z, degrees = true) {
    if (degrees) {
        x = Mathf.toRadians(x);
        y = Mathf.toRadians(y);
        z = Mathf.toRadians(z);
    }
    _worldEulerBuffer.set(x, y, z);
    _worldQuaternionBuffer.setFromEuler(_worldEulerBuffer);
    setWorldQuaternion(obj, _worldQuaternionBuffer);
}
// from https://github.com/mrdoob/js/pull/10995#issuecomment-287614722
export function logHierarchy(root, collapsible = true) {
    if (!root)
        return;
    if (collapsible) {
        (function printGraph(obj) {
            console.groupCollapsed((obj.name ? obj.name : '(no name : ' + obj.type + ')') + ' %o', obj);
            obj.children.forEach(printGraph);
            console.groupEnd();
        }(root));
    }
    else {
        root.traverse(function (obj) {
            var s = '|___';
            var obj2 = obj;
            while (obj2.parent !== null) {
                s = '\t' + s;
                obj2 = obj2.parent;
            }
            console.log(s + obj.name + ' <' + obj.type + '>');
        });
    }
    ;
}
export function getParentHierarchyPath(obj) {
    let path = obj?.name || "";
    if (!obj)
        return path;
    let parent = obj.parent;
    while (parent) {
        path = parent.name + "/" + path;
        parent = parent.parent;
    }
    return path;
}
export function isAnimationAction(obj) {
    if (obj) {
        // this doesnt work :(
        // return obj instanceof AnimationAction;
        // instead we do this:
        const act = obj;
        return act.blendMode !== undefined && act.clampWhenFinished !== undefined && act.enabled !== undefined && act.fadeIn !== undefined && act.getClip !== undefined;
    }
    return false;
}
class BlitMaterial extends ShaderMaterial {
    static vertex = `
varying vec2 vUv;
void main(){
    vUv = uv;
    gl_Position = vec4(position.xy, 0., 1.0);
}`;
    constructor() {
        super({
            vertexShader: BlitMaterial.vertex,
            uniforms: {
                map: new Uniform(null),
                flipY: new Uniform(true),
                writeDepth: new Uniform(false),
                depthTexture: new Uniform(null)
            },
            fragmentShader: `
uniform sampler2D map;
uniform bool flipY;
uniform bool writeDepth;
uniform sampler2D depthTexture;

varying vec2 vUv;

void main(){ 
    vec2 uv = vUv;
    if (flipY) uv.y = 1.0 - uv.y;
    gl_FragColor = texture2D(map, uv);

    if (writeDepth) {
        float depth = texture2D(depthTexture, uv).r;
        gl_FragDepth = depth;

        // float linearDepth = (depth - 0.99) * 100.0; // Enhance near 1.0 values
        // gl_FragColor = vec4(linearDepth, linearDepth, linearDepth, 1.0);
    }
}`
        });
    }
    reset() {
        this.uniforms.map.value = null;
        this.uniforms.flipY.value = true;
        this.uniforms.writeDepth.value = false;
        this.uniforms.depthTexture.value = null;
        this.needsUpdate = true;
        this.uniformsNeedUpdate = true;
    }
}
/**
 * Utility class to perform various graphics operations like copying textures to canvas
 */
export class Graphics {
    static planeGeometry = new PlaneGeometry(2, 2, 1, 1);
    static renderer = new WebGLRenderer({ antialias: false, alpha: true });
    static perspectiveCam = new PerspectiveCamera();
    static orthographicCam = new OrthographicCamera();
    static scene = new Scene();
    static blitMaterial = new BlitMaterial();
    static mesh = new Mesh(Graphics.planeGeometry, Graphics.blitMaterial);
    /**
     * Copy a texture to a new texture
     * @param texture the texture to copy
     * @param blitMaterial the material to use for copying (optional)
     * @returns the newly created, copied texture
    */
    static copyTexture(texture, blitMaterial) {
        // ensure that a blit material exists
        if (!blitMaterial) {
            blitMaterial = this.blitMaterial;
        }
        ;
        this.blitMaterial.reset();
        const material = blitMaterial || this.blitMaterial;
        // TODO: reset the uniforms...
        material.uniforms.map.value = texture;
        material.needsUpdate = true;
        material.uniformsNeedUpdate = true;
        // ensure that the blit material has the correct vertex shader
        const origVertexShader = material.vertexShader;
        material.vertexShader = BlitMaterial.vertex;
        const mesh = this.mesh;
        mesh.material = material;
        mesh.frustumCulled = false;
        this.scene.children.length = 0;
        this.scene.add(mesh);
        this.renderer.setSize(texture.image.width, texture.image.height);
        this.renderer.clear();
        this.renderer.render(this.scene, this.perspectiveCam);
        const tex = new Texture(this.renderer.domElement);
        tex.name = "Copy";
        tex.needsUpdate = true; // < important!
        // reset vertex shader
        material.vertexShader = origVertexShader;
        return tex;
    }
    static blit(src, target, options) {
        const { renderer = this.renderer, blitMaterial = this.blitMaterial, flipY = false, depthTexture = null, depthTest = true, depthWrite = true, } = options || {};
        this.blitMaterial.reset();
        if (blitMaterial.uniforms.map)
            blitMaterial.uniforms.map.value = src;
        if (blitMaterial.uniforms.flipY)
            blitMaterial.uniforms.flipY.value = flipY;
        if (depthTexture) {
            blitMaterial.uniforms.writeDepth = new Uniform(true);
            blitMaterial.uniforms.depthTexture.value = depthTexture;
        }
        else {
            blitMaterial.uniforms.writeDepth = new Uniform(false);
            blitMaterial.uniforms.depthTexture.value = null;
        }
        blitMaterial.needsUpdate = true;
        blitMaterial.uniformsNeedUpdate = true;
        const mesh = this.mesh;
        mesh.material = blitMaterial;
        mesh.frustumCulled = false;
        this.scene.children.length = 0;
        this.scene.add(mesh);
        const renderTarget = renderer.getRenderTarget();
        // Set state
        const gl = renderer.getContext();
        if (depthTest)
            gl.enable(gl.DEPTH_TEST);
        else
            gl.disable(gl.DEPTH_TEST);
        renderer.state.buffers.depth.setMask(depthWrite);
        renderer.setClearColor(new Color(0, 0, 0), 0);
        if (renderer.pixelRatio !== window.devicePixelRatio) {
            if (renderer.xr.isPresenting === false)
                renderer.setPixelRatio(window.devicePixelRatio);
        }
        renderer.setRenderTarget(target);
        renderer.clear();
        renderer.render(this.scene, this.perspectiveCam);
        // Restore defaults (not using gl.getParameter to save/restore — it causes a synchronous GPU-CPU stall)
        renderer.setRenderTarget(renderTarget);
        gl.enable(gl.DEPTH_TEST);
        renderer.state.buffers.depth.setMask(true);
    }
    /**
     * Copy a texture to a HTMLCanvasElement
     * @param texture the texture convert
     * @param force if true the texture will be copied to a new texture before converting
     * @returns the HTMLCanvasElement with the texture or null if the texture could not be copied
     */
    static textureToCanvas(texture, force = false) {
        if (!texture) {
            return null;
        }
        if (force === true || texture["isCompressedTexture"] === true) {
            texture = copyTexture(texture);
        }
        const image = texture.image;
        if (isImageBitmap(image)) {
            const canvas = document.createElement('canvas');
            canvas.width = image.width;
            canvas.height = image.height;
            const context = canvas.getContext('2d');
            if (!context) {
                console.error("Failed getting canvas 2d context");
                return null;
            }
            context.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
            return canvas;
        }
        return null;
    }
}
/**@obsolete use Graphics.copyTexture */
export function copyTexture(texture) {
    return Graphics.copyTexture(texture);
}
/**@obsolete use Graphics.textureToCanvas */
export function textureToCanvas(texture, force = false) {
    return Graphics.textureToCanvas(texture, force);
}
;
function isImageBitmap(image) {
    return (typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
        (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
        (typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) ||
        (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap);
}
function isMesh(obj) {
    const type = obj.type;
    // Note: we don't use the instanceof check here because it's unreliable in certain minification scenarios where it returns false
    return type === "Mesh" || type === "SkinnedMesh";
}
// for contact shadows
export function setVisibleInCustomShadowRendering(obj, enabled) {
    if (enabled)
        obj["needle:rendercustomshadow"] = true;
    else {
        obj["needle:rendercustomshadow"] = false;
    }
}
export function getVisibleInCustomShadowRendering(obj) {
    if (obj) {
        if (obj["needle:rendercustomshadow"] === true) {
            return true;
        }
        else if (obj["needle:rendercustomshadow"] == undefined) {
            return true;
        }
    }
    return false;
}
/**
 * Get the axis-aligned bounding box of a list of objects.
 * @param objects The objects to get the bounding box from.
 * @param ignore Objects to ignore when calculating the bounding box. Objects that are invisible (gizmos, helpers, etc.) are excluded by default.
 * @param layers The layers to include. Typically the main camera's layers.
 * @param result The result box to store the bounding box in. Returns a new box if not passed in.
 */
export function getBoundingBox(objects, ignore = undefined, layers = undefined, result = undefined) {
    const box = result || new Box3();
    box.makeEmpty();
    const emptyChildren = [];
    function expandByObjectRecursive(obj) {
        let allowExpanding = true;
        // we dont want to check invisible objects
        if (!obj.visible)
            return;
        if (useForAutoFit(obj) === false)
            return;
        if (obj.type === "TransformControlsGizmo" || obj.type === "TransformControlsPlane")
            return;
        // ignore Box3Helpers
        if (obj instanceof Box3Helper)
            allowExpanding = false;
        if (obj instanceof GridHelper)
            allowExpanding = false;
        // ignore GroundProjectedEnv
        if (obj instanceof GroundedSkybox)
            allowExpanding = false;
        if (obj.isGizmo === true)
            allowExpanding = false;
        // // Ignore shadow catcher geometry
        if (obj.material instanceof ShadowMaterial)
            allowExpanding = false;
        // ONLY fit meshes
        if (!(isMesh(obj)))
            allowExpanding = false;
        // Layer test, typically with the main camera
        if (layers && obj.layers.test(layers) === false)
            allowExpanding = false;
        if (allowExpanding) {
            // Ignore things parented to the camera + ignore the camera
            if (ignore && Array.isArray(ignore) && ignore?.includes(obj))
                return;
            else if (typeof ignore === "function") {
                if (ignore(obj) === true)
                    return;
            }
        }
        // We don't want to fit UI objects
        if (obj["isUI"] === true)
            return;
        // If we encountered some geometry that should be ignored
        // Then we don't want to use that for expanding the view
        if (allowExpanding) {
            // Temporary override children
            const children = obj.children;
            obj.children = emptyChildren;
            // TODO: validate that object doesn't contain NaN values
            const pos = obj.position;
            const scale = obj.scale;
            if (Number.isNaN(pos.x) || Number.isNaN(pos.y) || Number.isNaN(pos.z)) {
                console.warn(`Object \"${obj.name}\" has NaN values in position or scale.... will ignore it`, pos, scale);
                return;
            }
            // Sanitize for the three.js method that only checks for undefined here
            // @ts-ignore
            if (obj.geometry === null)
                obj.geometry = undefined;
            box.expandByObject(obj, true);
            obj.children = children;
        }
        for (const child of obj.children) {
            expandByObjectRecursive(child);
        }
    }
    let hasAnyObject = false;
    if (!Array.isArray(objects))
        objects = [objects];
    for (const object of objects) {
        if (!object)
            continue;
        hasAnyObject = true;
        object.updateMatrixWorld();
        expandByObjectRecursive(object);
    }
    if (!hasAnyObject) {
        console.warn("No objects to fit camera to...");
        return box;
    }
    return box;
}
/**
 * Fits an object into a bounding volume. The volume is defined by a Box3 in world space.
 * @param obj the object to fit
 * @param volume the volume to fit the object into
 * @param opts options for fitting
 */
export function fitObjectIntoVolume(obj, volume, opts) {
    const box = getBoundingBox([obj], opts?.ignore);
    const boundsSize = new Vector3();
    box.getSize(boundsSize);
    const boundsCenter = new Vector3();
    box.getCenter(boundsCenter);
    const targetSize = new Vector3();
    volume.getSize(targetSize);
    const targetCenter = new Vector3();
    volume.getCenter(targetCenter);
    const scale = new Vector3();
    scale.set(targetSize.x / boundsSize.x, targetSize.y / boundsSize.y, targetSize.z / boundsSize.z);
    const minScale = Math.min(scale.x, scale.y, scale.z);
    const useScale = opts?.scale !== false;
    if (useScale) {
        setWorldScale(obj, getWorldScale(obj).multiplyScalar(minScale));
    }
    if (opts?.position !== false) {
        const boundsBottomPosition = new Vector3();
        box.getCenter(boundsBottomPosition);
        boundsBottomPosition.y = box.min.y;
        const targetBottomPosition = new Vector3();
        volume.getCenter(targetBottomPosition);
        targetBottomPosition.y = volume.min.y;
        const offset = targetBottomPosition.clone().sub(boundsBottomPosition);
        if (useScale)
            offset.multiplyScalar(minScale);
        setWorldPosition(obj, getWorldPosition(obj).add(offset));
    }
    return {
        boundsBefore: box,
        scale,
    };
}
/**
 * Place an object on a surface. This will calculate the object bounds which might be an expensive operation for complex objects.
 * The object will be visually placed on the surface (the object's pivot will be ignored).
 * @param obj the object to place on the surface
 * @param point the point to place the object on
 * @returns the offset from the object bounds to the pivot
 */
export function placeOnSurface(obj, point) {
    const bounds = getBoundingBox([obj]);
    const center = new Vector3();
    bounds.getCenter(center);
    center.y = bounds.min.y;
    const offset = point.clone().sub(center);
    const worldPos = getWorldPosition(obj);
    setWorldPosition(obj, worldPos.add(offset));
    return {
        offset,
        bounds,
    };
}
/**
 * Postprocesses the material of an object loaded by {@link FBXLoader}.
 * It will apply necessary color conversions, remap shininess to roughness, and turn everything into {@link MeshStandardMaterial} on the object.
 * This ensures consistent lighting and shading, including environment effects.
 */
export function postprocessFBXMaterials(obj, material, index, array) {
    if (Array.isArray(material)) {
        let success = true;
        for (let i = 0; i < material.length; i++) {
            const res = postprocessFBXMaterials(obj, material[i], i, material);
            if (!res)
                success = false;
        }
        return success;
    }
    // ignore if the material is already a MeshStandardMaterial
    if (material.type === "MeshStandardMaterial" || material.type === "MeshBasicMaterial") {
        return false;
    }
    // check if the material was already processed
    else if (material["material:fbx"] != undefined) {
        return true;
    }
    const newMaterial = new MeshStandardMaterial();
    newMaterial["material:fbx"] = material;
    const oldMaterial = material;
    if (oldMaterial) {
        // If a map is present then the FBX color should be ignored
        // Tested e.g. in Unity and Substance Stager
        // https://docs.unity3d.com/2020.1/Documentation/Manual/FBXImporter-Materials.html#:~:text=If%20a%20diffuse%20Texture%20is%20set%2C%20it%20ignores%20the%20diffuse%20color%20(this%20matches%20how%20it%20works%20in%20Autodesk®%20Maya®%20and%20Autodesk®%203ds%20Max®)
        if (!oldMaterial.map)
            newMaterial.color.copyLinearToSRGB(oldMaterial.color);
        else
            newMaterial.color.set(1, 1, 1);
        newMaterial.emissive.copyLinearToSRGB(oldMaterial.emissive);
        newMaterial.emissiveIntensity = oldMaterial.emissiveIntensity;
        newMaterial.opacity = oldMaterial.opacity;
        newMaterial.displacementScale = oldMaterial.displacementScale;
        newMaterial.transparent = oldMaterial.transparent;
        newMaterial.bumpMap = oldMaterial.bumpMap;
        newMaterial.aoMap = oldMaterial.aoMap;
        newMaterial.map = oldMaterial.map;
        newMaterial.displacementMap = oldMaterial.displacementMap;
        newMaterial.emissiveMap = oldMaterial.emissiveMap;
        newMaterial.normalMap = oldMaterial.normalMap;
        newMaterial.envMap = oldMaterial.envMap;
        newMaterial.alphaMap = oldMaterial.alphaMap;
        newMaterial.metalness = oldMaterial.reflectivity;
        newMaterial.vertexColors = oldMaterial.vertexColors;
        if (oldMaterial.shininess) {
            // from blender source code 
            // https://github.com/blender/blender-addons/blob/5e66092bcbe0df6855b3fa814b4826add8b01360/io_scene_fbx/import_fbx.py#L1442
            // https://github.com/blender/blender-addons/blob/main/io_scene_fbx/import_fbx.py#L2060
            newMaterial.roughness = 1.0 - (Math.sqrt(oldMaterial.shininess) / 10.0);
        }
        newMaterial.needsUpdate = true;
    }
    if (index === undefined) {
        obj.material = newMaterial;
    }
    else {
        array[index] = newMaterial;
    }
    return true;
}
//# sourceMappingURL=engine_three_utils.js.map