Source: scene/mesh/shared/Mesh.ts

import { pointInTriangle } from '../../../maths/point/pointInTriangle';
import { Geometry } from '../../../rendering/renderers/shared/geometry/Geometry';
import { State } from '../../../rendering/renderers/shared/state/State';
import { Texture } from '../../../rendering/renderers/shared/texture/Texture';
import { deprecation, v8_0_0 } from '../../../utils/logging/deprecation';
import { Container } from '../../container/Container';
import { MeshGeometry } from './MeshGeometry';

import type { PointData } from '../../../maths/point/PointData';
import type { Topology } from '../../../rendering/renderers/shared/geometry/const';
import type { Instruction } from '../../../rendering/renderers/shared/instructions/Instruction';
import type { Shader } from '../../../rendering/renderers/shared/shader/Shader';
import type { View } from '../../../rendering/renderers/shared/view/View';
import type { Bounds } from '../../container/bounds/Bounds';
import type { ContainerOptions } from '../../container/Container';
import type { DestroyOptions } from '../../container/destroyTypes';

export interface TextureShader extends Shader
{
    texture: Texture;
}

/**
 * Constructor options used for `Mesh` instances. Extends scene.MeshViewOptions
 * ```js
 * const mesh = new Mesh({
 *    texture: Texture.from('assets/image.png'),
 *    geometry: new PlaneGeometry(),
 *    shader: Shader.from(VERTEX, FRAGMENT),
 * });
 * ```
 * @see Mesh
 * @see scene.MeshViewOptions
 * @memberof scene
 */

/**
 * @memberof scene
 */
export interface MeshOptions<
    GEOMETRY extends Geometry = MeshGeometry,
    SHADER extends Shader = TextureShader
> extends ContainerOptions
{
    /**
     * Includes vertex positions, face indices, colors, UVs, and
     * custom attributes within buffers, reducing the cost of passing all
     * this data to the GPU. Can be shared between multiple Mesh objects.
     */
    geometry: GEOMETRY;
    /**
     * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU.
     * Can be shared between multiple Mesh objects.
     */
    shader?: SHADER;
    /** The state of WebGL required to render the mesh. */
    state?: State;
    /** The texture that the Mesh uses. Null for non-MeshMaterial shaders */
    texture?: Texture;
    /** Whether or not to round the x/y position. */
    roundPixels?: boolean;
}
/**
 * Base mesh class.
 *
 * This class empowers you to have maximum flexibility to render any kind of WebGL/WebGPU visuals you can think of.
 * This class assumes a certain level of WebGL/WebGPU knowledge.
 * If you know a bit this should abstract enough away to make your life easier!
 *
 * Pretty much ALL WebGL/WebGPU can be broken down into the following:
 * - Geometry - The structure and data for the mesh. This can include anything from positions, uvs, normals, colors etc..
 * - Shader - This is the shader that PixiJS will render the geometry with (attributes in the shader must match the geometry)
 * - State - This is the state of WebGL required to render the mesh.
 *
 * Through a combination of the above elements you can render anything you want, 2D or 3D!
 * @memberof scene
 */
export class Mesh<
    GEOMETRY extends Geometry = MeshGeometry,
    SHADER extends Shader = TextureShader
> extends Container implements View, Instruction
{
    public readonly renderPipeId = 'mesh';
    public readonly canBundle = true;
    public state: State;

    /** @ignore */
    public _texture: Texture;
    /** @ignore */
    public _geometry: GEOMETRY;
    /** @ignore */
    public _shader?: SHADER;

    public _roundPixels: 0 | 1 = 0;

    /**
     * @param {scene.MeshOptions} options - options for the mesh instance
     */
    constructor(options: MeshOptions<GEOMETRY, SHADER>);
    /** @deprecated since 8.0.0 */
    constructor(geometry: GEOMETRY, shader: SHADER, state?: State, drawMode?: Topology);
    constructor(...args: [MeshOptions<GEOMETRY, SHADER>] | [GEOMETRY, SHADER, State?, Topology?])
    {
        let options = args[0];

        if (options instanceof Geometry)
        {
            // #if _DEBUG
            deprecation(v8_0_0, 'Mesh: use new Mesh({ geometry, shader }) instead');
            // #endif

            options = {
                geometry: options,
                shader: args[1],
            } as MeshOptions<GEOMETRY, SHADER>;

            if (args[3])
            {
                // #if _DEBUG
                deprecation(v8_0_0, 'Mesh: drawMode argument has been removed, use geometry.topology instead');
                // #endif

                options.geometry.topology = args[3];
            }
        }

        const { geometry, shader, texture, roundPixels, state, ...rest } = options;

        super({
            label: 'Mesh',
            ...rest
        });

        this.allowChildren = false;

        this.shader = shader;
        this.texture = texture ?? (shader as unknown as TextureShader)?.texture ?? Texture.WHITE;
        this.state = state ?? State.for2d();

        this._geometry = geometry;
        this._geometry.on('update', this.onViewUpdate, this);

        this.roundPixels = roundPixels ?? false;
    }

    /**
     *  Whether or not to round the x/y position of the mesh.
     * @type {boolean}
     */
    get roundPixels()
    {
        return !!this._roundPixels;
    }

    set roundPixels(value: boolean)
    {
        this._roundPixels = value ? 1 : 0;
    }

    /** Alias for shader. */
    get material()
    {
        // #if _DEBUG
        deprecation(v8_0_0, 'mesh.material property has been removed, use mesh.shader instead');
        // #endif

        return this._shader;
    }

    /**
     * Represents the vertex and fragment shaders that processes the geometry and runs on the GPU.
     * Can be shared between multiple Mesh objects.
     */
    set shader(value: SHADER)
    {
        if (this._shader === value) return;

        this._shader = value;
        this.onViewUpdate();
    }

    get shader()
    {
        return this._shader;
    }

    /**
     * Includes vertex positions, face indices, colors, UVs, and
     * custom attributes within buffers, reducing the cost of passing all
     * this data to the GPU. Can be shared between multiple Mesh objects.
     */
    set geometry(value: GEOMETRY)
    {
        if (this._geometry === value) return;

        this._geometry?.off('update', this.onViewUpdate, this);
        value.on('update', this.onViewUpdate, this);

        this._geometry = value;
        this.onViewUpdate();
    }

    get geometry()
    {
        return this._geometry;
    }

    /** The texture that the Mesh uses. Null for non-MeshMaterial shaders */
    set texture(value: Texture)
    {
        value ||= Texture.EMPTY;

        const currentTexture = this._texture;

        if (currentTexture === value) return;

        if (currentTexture && currentTexture.dynamic) currentTexture.off('update', this.onViewUpdate, this);
        if (value.dynamic) value.on('update', this.onViewUpdate, this);

        if (this.shader)
        {
            (this.shader as unknown as TextureShader).texture = value;
        }

        this._texture = value;
        this.onViewUpdate();
    }

    get texture()
    {
        return this._texture;
    }

    get batched()
    {
        if (this._shader) return false;

        if (this._geometry instanceof MeshGeometry)
        {
            if (this._geometry.batchMode === 'auto')
            {
                return this._geometry.positions.length / 2 <= 100;
            }

            return this._geometry.batchMode === 'batch';
        }

        return false;
    }

    /**
     * The local bounds of the mesh.
     * @type {rendering.Bounds}
     */
    get bounds()
    {
        return this._geometry.bounds;
    }

    /**
     * Adds the bounds of this object to the bounds object.
     * @param bounds - The output bounds object.
     */
    public addBounds(bounds: Bounds)
    {
        bounds.addBounds(this.geometry.bounds);
    }

    /**
     * Checks if the object contains the given point.
     * @param point - The point to check
     */
    public containsPoint(point: PointData)
    {
        const { x, y } = point;

        if (!this.bounds.containsPoint(x, y)) return false;

        const vertices = this.geometry.getBuffer('aPosition').data;

        const step = this.geometry.topology === 'triangle-strip' ? 3 : 1;

        if (this.geometry.getIndex())
        {
            const indices = this.geometry.getIndex().data;
            const len = indices.length;

            for (let i = 0; i + 2 < len; i += step)
            {
                const ind0 = indices[i] * 2;
                const ind1 = indices[i + 1] * 2;
                const ind2 = indices[i + 2] * 2;

                if (pointInTriangle(
                    x, y,
                    vertices[ind0],
                    vertices[ind0 + 1],
                    vertices[ind1],
                    vertices[ind1 + 1],
                    vertices[ind2],
                    vertices[ind2 + 1],
                ))
                {
                    return true;
                }
            }
        }
        else
        {
            const len = vertices.length / 2; // Each vertex has 2 coordinates, x and y

            for (let i = 0; i + 2 < len; i += step)
            {
                const ind0 = i * 2;
                const ind1 = (i + 1) * 2;
                const ind2 = (i + 2) * 2;

                if (pointInTriangle(
                    x, y,
                    vertices[ind0],
                    vertices[ind0 + 1],
                    vertices[ind1],
                    vertices[ind1 + 1],
                    vertices[ind2],
                    vertices[ind2 + 1],
                ))
                {
                    return true;
                }
            }
        }

        return false;
    }

    /** @ignore */
    public onViewUpdate()
    {
        // increment from the 12th bit!
        this._didChangeId += 1 << 12;

        if (this.didViewUpdate) return;
        this.didViewUpdate = true;

        const renderGroup = this.renderGroup || this.parentRenderGroup;

        if (renderGroup)
        {
            renderGroup.onChildViewUpdate(this);
        }
    }

    /**
     * Destroys this sprite renderable and optionally its texture.
     * @param options - Options parameter. A boolean will act as if all options
     *  have been set to that value
     * @param {boolean} [options.texture=false] - Should it destroy the current texture of the renderable as well
     * @param {boolean} [options.textureSource=false] - Should it destroy the textureSource of the renderable as well
     */
    public destroy(options?: DestroyOptions): void
    {
        super.destroy(options);

        const destroyTexture = typeof options === 'boolean' ? options : options?.texture;

        if (destroyTexture)
        {
            const destroyTextureSource = typeof options === 'boolean' ? options : options?.textureSource;

            this._texture.destroy(destroyTextureSource);
        }

        this._geometry?.off('update', this.onViewUpdate, this);

        this._texture = null;
        this._geometry = null;
        this._shader = null;
    }
}