Source: rendering/renderers/shared/texture/TextureStyle.ts

import EventEmitter from 'eventemitter3';
import { uid } from '../../../../utils/data/uid';
import { deprecation, v8_0_0 } from '../../../../utils/logging/deprecation';

import type { BindResource } from '../../gpu/shader/BindResource';
import type { COMPARE_FUNCTION, SCALE_MODE, WRAP_MODE } from './const';

const idHash: Record<string, number> = Object.create(null);

/**
 * This takes a shader string and maps it to a resource id.
 * This is a little different than regular resource ids as these ids
 * are not unique to the resource. But must not overlap with other (non sampler) resources Ids.
 * @param value - the string to turn into a resource id
 * @returns a unique resource id
 */
function createResourceIdFromString(value: string): number
{
    const id = idHash[value];

    if (id === undefined)
    {
        idHash[value] = uid('resource');
    }

    return id;
}

export interface TextureStyleOptions extends Partial<TextureStyle>
{
    /** setting this will set wrapModeU,wrapModeV and wrapModeW all at once! */
    addressMode?: WRAP_MODE;
    /** specifies the {{GPUAddressMode|address modes}} for the texture width, height, and depth coordinates, respectively. */
    addressModeU?: WRAP_MODE;
    /** specifies the {{GPUAddressMode|address modes}} for the texture width, height, and depth coordinates, respectively. */
    addressModeV?: WRAP_MODE;
    /** Specifies the {{GPUAddressMode|address modes}} for the texture width, height, and depth coordinates, respectively. */
    addressModeW?: WRAP_MODE;

    /** setting this will set magFilter,minFilter and mipmapFilter all at once!  */
    scaleMode?: SCALE_MODE;

    /** specifies the sampling behavior when the sample footprint is smaller than or equal to one texel. */
    magFilter?: SCALE_MODE;
    /** specifies the sampling behavior when the sample footprint is larger than one texel. */
    minFilter?: SCALE_MODE;
    /** specifies behavior for sampling between mipmap levels. */
    mipmapFilter?: SCALE_MODE;

    /** specifies the minimum and maximum levels of detail, respectively, used internally when sampling a texture. */
    lodMinClamp?: number;
    /** Specifies the minimum and maximum levels of detail, respectively, used internally when sampling a texture. */
    lodMaxClamp?: number;
    /**
     * When provided the sampler will be a comparison sampler with the specified
     * GPUCompareFunction.
     * Note: Comparison samplers may use filtering, but the sampling results will be
     * implementation-dependent and may differ from the normal filtering rules.
     */
    compare?: COMPARE_FUNCTION;
    /**
     * Specifies the maximum anisotropy value clamp used by the sampler.
     * Note: Most implementations support GPUSamplerDescriptor#maxAnisotropy values in range
     * between 1 and 16, inclusive. The used value of GPUSamplerDescriptor#maxAnisotropy will
     * be clamped to the maximum value that the platform supports.
     *
     * setting this to anything higher than 1 will set scale modes to 'linear'
     */
    maxAnisotropy?: number;
}

/**
 * A texture style describes how a texture should be sampled by a shader.
 * @memberof rendering
 */
export class TextureStyle extends EventEmitter<{
    change: TextureStyle,
    destroy: TextureStyle,
}> implements BindResource
{
    public _resourceType = 'textureSampler';
    public _touched = 0;
    private _sharedResourceId: number;

    /** default options for the style */
    public static readonly defaultOptions: TextureStyleOptions = {
        addressMode: 'clamp-to-edge',
        scaleMode: 'linear'
    };

    /** */
    public addressModeU?: WRAP_MODE;
    /** */
    public addressModeV?: WRAP_MODE;
    /** Specifies the {{GPUAddressMode|address modes}} for the texture width, height, and depth coordinates, respectively. */
    public addressModeW?: WRAP_MODE;
    /** Specifies the sampling behavior when the sample footprint is smaller than or equal to one texel. */
    public magFilter?: SCALE_MODE;
    /** Specifies the sampling behavior when the sample footprint is larger than one texel. */
    public minFilter?: SCALE_MODE;
    /** Specifies behavior for sampling between mipmap levels. */
    public mipmapFilter?: SCALE_MODE;
    /** */
    public lodMinClamp?: number;
    /** Specifies the minimum and maximum levels of detail, respectively, used internally when sampling a texture. */
    public lodMaxClamp?: number;
    /**
     * When provided the sampler will be a comparison sampler with the specified
     * GPUCompareFunction.
     * Note: Comparison samplers may use filtering, but the sampling results will be
     * implementation-dependent and may differ from the normal filtering rules.
     */
    public compare?: COMPARE_FUNCTION;
    /**
     * Specifies the maximum anisotropy value clamp used by the sampler.
     * Note: Most implementations support GPUSamplerDescriptor#maxAnisotropy values in range
     * between 1 and 16, inclusive. The used value of GPUSamplerDescriptor#maxAnisotropy will
     * be clamped to the maximum value that the platform supports.
     * @internal
     * @ignore
     */
    public _maxAnisotropy?: number = 1;

    /**
     * Has the style been destroyed?
     * @readonly
     */
    public destroyed = false;

    /**
     * @param options - options for the style
     */
    constructor(options: TextureStyleOptions = {})
    {
        super();

        options = { ...TextureStyle.defaultOptions, ...options };

        this.addressMode = options.addressMode;

        this.addressModeU = options.addressModeU ?? this.addressModeU;
        this.addressModeV = options.addressModeV ?? this.addressModeV;
        this.addressModeW = options.addressModeW ?? this.addressModeW;

        this.scaleMode = options.scaleMode;

        this.magFilter = options.magFilter ?? this.magFilter;
        this.minFilter = options.minFilter ?? this.minFilter;
        this.mipmapFilter = options.mipmapFilter ?? this.mipmapFilter;

        this.lodMinClamp = options.lodMinClamp;
        this.lodMaxClamp = options.lodMaxClamp;

        this.compare = options.compare;

        this.maxAnisotropy = options.maxAnisotropy ?? 1;
    }

    set addressMode(value: WRAP_MODE)
    {
        this.addressModeU = value;
        this.addressModeV = value;
        this.addressModeW = value;
    }

    /** setting this will set wrapModeU,wrapModeV and wrapModeW all at once! */
    get addressMode(): WRAP_MODE
    {
        return this.addressModeU;
    }

    set wrapMode(value: WRAP_MODE)
    {
        // #if _DEBUG
        deprecation(v8_0_0, 'TextureStyle.wrapMode is now TextureStyle.addressMode');
        // #endif

        this.addressMode = value;
    }

    get wrapMode(): WRAP_MODE
    {
        return this.addressMode;
    }

    set scaleMode(value: SCALE_MODE)
    {
        this.magFilter = value;
        this.minFilter = value;
        this.mipmapFilter = value;
    }

    /** setting this will set magFilter,minFilter and mipmapFilter all at once!  */
    get scaleMode(): SCALE_MODE
    {
        return this.magFilter;
    }

    /** Specifies the maximum anisotropy value clamp used by the sampler. */
    set maxAnisotropy(value: number)
    {
        this._maxAnisotropy = Math.min(value, 16);

        if (this._maxAnisotropy > 1)
        {
            this.scaleMode = 'linear';
        }
    }

    get maxAnisotropy(): number
    {
        return this._maxAnisotropy;
    }

    // TODO - move this to WebGL?
    get _resourceId(): number
    {
        return this._sharedResourceId || this._generateResourceId();
    }

    public update()
    {
        // manage the resource..
        this.emit('change', this);
        this._sharedResourceId = null;
    }

    private _generateResourceId(): number
    {
        // eslint-disable-next-line max-len
        const bigKey = `${this.addressModeU}-${this.addressModeV}-${this.addressModeW}-${this.magFilter}-${this.minFilter}-${this.mipmapFilter}-${this.lodMinClamp}-${this.lodMaxClamp}-${this.compare}-${this._maxAnisotropy}`;

        this._sharedResourceId = createResourceIdFromString(bigKey);

        return this._resourceId;
    }

    /** Destroys the style */
    public destroy()
    {
        this.destroyed = true;

        this.emit('destroy', this);
        this.emit('change', this);

        this.removeAllListeners();
    }
}