import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetAssetManager, GetAvatarRenderManager, IFigurePart, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroRectangle, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; import { IAvatarEditorCategoryPartItem } from './IAvatarEditorCategoryPartItem'; const MAX_CACHE_BYTES = 200 * 1024 * 1024; class LRUImageCache { private _cache: Map = new Map(); private _currentBytes: number = 0; public get(key: string): string | undefined { const value = this._cache.get(key); if(value !== undefined) { this._cache.delete(key); this._cache.set(key, value); } return value; } public set(key: string, value: string): void { if(this._cache.has(key)) { const old = this._cache.get(key); this._currentBytes -= (key.length + old.length) * 2; this._cache.delete(key); } const entryBytes = (key.length + value.length) * 2; while(this._currentBytes + entryBytes > MAX_CACHE_BYTES && this._cache.size > 0) { const firstKey = this._cache.keys().next().value; const firstValue = this._cache.get(firstKey); this._currentBytes -= (firstKey.length + firstValue.length) * 2; this._cache.delete(firstKey); } this._cache.set(key, value); this._currentBytes += entryBytes; } public clear(): void { this._cache.clear(); this._currentBytes = 0; } } export class AvatarEditorThumbnailsHelper { private static THUMBNAIL_CACHE: LRUImageCache = new LRUImageCache(); private static THUMB_DIRECTIONS: number[] = [ 2, 6, 0, 4, 3, 1 ]; private static ALPHA_FILTER: NitroAlphaFilter = new NitroAlphaFilter({ alpha: 0.2 }); private static DRAW_ORDER: string[] = [ AvatarFigurePartType.LEFT_HAND_ITEM, AvatarFigurePartType.LEFT_HAND, AvatarFigurePartType.LEFT_SLEEVE, AvatarFigurePartType.LEFT_COAT_SLEEVE, AvatarFigurePartType.BODY, AvatarFigurePartType.SHOES, AvatarFigurePartType.LEGS, AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_ACCESSORY, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.WAIST_ACCESSORY, AvatarFigurePartType.RIGHT_HAND, AvatarFigurePartType.RIGHT_SLEEVE, AvatarFigurePartType.RIGHT_COAT_SLEEVE, AvatarFigurePartType.HEAD, AvatarFigurePartType.FACE, AvatarFigurePartType.EYES, AvatarFigurePartType.HAIR, AvatarFigurePartType.HAIR_BIG, AvatarFigurePartType.FACE_ACCESSORY, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.RIGHT_HAND_ITEM, AvatarFigurePartType.PET, 'ptl', 'ptr', AvatarFigurePartType.MISC, 'mcl', 'mcr', ]; private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem, partColors?: IPartColor[], isDisabled?: boolean): string { let key = `${ setType }-${ part.partSet.id }`; if(partColors?.length) { key += '-' + partColors.map(c => c?.rgb?.toString(16) ?? '0').join(','); } if(isDisabled) key += '-d'; return key; } public static clearCache(): void { this.THUMBNAIL_CACHE.clear(); } public static async build(setType: string, part: IAvatarEditorCategoryPartItem, useColors: boolean, partColors: IPartColor[], isDisabled: boolean = false): Promise { if(!setType || !setType.length || !part || !part.partSet || !part.partSet.parts || !part.partSet.parts.length) return null; const thumbnailKey = this.getThumbnailKey(setType, part, useColors ? partColors : null, isDisabled); const cached = this.THUMBNAIL_CACHE.get(thumbnailKey); if(cached) return cached; const buildContainer = (part: IAvatarEditorCategoryPartItem, useColors: boolean, partColors: IPartColor[], isDisabled: boolean = false) => { const container = new NitroContainer(); const parts = part.partSet.parts.concat().sort(this.sortByDrawOrder); for(const part of parts) { if(!part) continue; let asset: IGraphicAsset = null; let direction = 0; let hasAsset = false; while(!hasAsset && (direction < AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS.length)) { const assetName = `${ AvatarFigurePartType.SCALE }_${ AvatarFigurePartType.STD }_${ part.type }_${ part.id }_${ AvatarEditorThumbnailsHelper.THUMB_DIRECTIONS[direction] }_${ AvatarFigurePartType.DEFAULT_FRAME }`; asset = GetAssetManager().getAsset(assetName); if(asset && asset.texture) { hasAsset = true; } else { direction++; } } if(!hasAsset) { continue; } const x = asset.offsetX; const y = asset.offsetY; const sprite = new NitroSprite(asset.texture); sprite.position.set(x, y); if(useColors && (part.colorLayerIndex > 0) && partColors && partColors.length) { const color = partColors[(part.colorLayerIndex - 1)]; if(color) sprite.tint = color.rgb; } if(isDisabled) container.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; container.addChild(sprite); } return container; }; return new Promise(async (resolve, reject) => { const resetFigure = async (figure: string) => { const container = buildContainer(part, useColors, partColors, isDisabled); const imageUrl = await TextureUtils.generateImageUrl({ target: container, resolution: 1 }); if(imageUrl) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); resolve(imageUrl); }; const figureContainer = GetAvatarRenderManager().createFigureContainer(`${ setType }-${ part.partSet.id }`); if(!GetAvatarRenderManager().isFigureContainerReady(figureContainer)) { GetAvatarRenderManager().downloadAvatarFigure(figureContainer, { resetFigure, dispose: null, disposed: false }); } else { resetFigure(null); } }); } public static async buildForFace(figureString: string, isDisabled: boolean = false): Promise { if(!figureString || !figureString.length) return null; const thumbnailKey = figureString + (isDisabled ? '-d' : ''); const cached = this.THUMBNAIL_CACHE.get(thumbnailKey); if(cached) return cached; return new Promise(async (resolve, reject) => { const resetFigure = async (figure: string) => { const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { resetFigure, dispose: null, disposed: false }); if(avatarImage.isPlaceholder()) return; const texture = avatarImage.processAsTexture(AvatarSetType.HEAD, false); const sprite = new NitroSprite(texture); if(isDisabled) sprite.filters = [ AvatarEditorThumbnailsHelper.ALPHA_FILTER ]; const imageUrl = await TextureUtils.generateImageUrl({ target: sprite, frame: new NitroRectangle(0, 0, texture.width, texture.height) }); sprite.destroy(); avatarImage.dispose(); AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); resolve(imageUrl); }; resetFigure(figureString); }); } private static sortByDrawOrder(a: IFigurePart, b: IFigurePart): number { const indexA = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(a.type); const indexB = AvatarEditorThumbnailsHelper.DRAW_ORDER.indexOf(b.type); if(indexA < indexB) return -1; if(indexA > indexB) return 1; if(a.index < b.index) return -1; if(a.index > b.index) return 1; return 0; } }