diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index bffc418..ad0db38 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -1,9 +1,61 @@ 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: Map = new Map(); + 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[] = [ @@ -37,9 +89,18 @@ export class AvatarEditorThumbnailsHelper 'ptr', ]; - private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem): string + private static getThumbnailKey(setType: string, part: IAvatarEditorCategoryPartItem, partColors?: IPartColor[], isDisabled?: boolean): string { - return `${ setType }-${ part.partSet.id }`; + 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 @@ -51,7 +112,7 @@ export class AvatarEditorThumbnailsHelper { if(!setType || !setType.length || !part || !part.partSet || !part.partSet.parts || !part.partSet.parts.length) return null; - const thumbnailKey = this.getThumbnailKey(setType, part); + const thumbnailKey = this.getThumbnailKey(setType, part, useColors ? partColors : null, isDisabled); const cached = this.THUMBNAIL_CACHE.get(thumbnailKey); if(cached) return cached; @@ -145,7 +206,7 @@ export class AvatarEditorThumbnailsHelper { if(!figureString || !figureString.length) return null; - const thumbnailKey = figureString; + const thumbnailKey = figureString + (isDisabled ? '-d' : ''); const cached = this.THUMBNAIL_CACHE.get(thumbnailKey); if(cached) return cached; diff --git a/src/common/layout/LayoutAvatarImageView.tsx b/src/common/layout/LayoutAvatarImageView.tsx index 2b3b156..853d361 100644 --- a/src/common/layout/LayoutAvatarImageView.tsx +++ b/src/common/layout/LayoutAvatarImageView.tsx @@ -2,6 +2,7 @@ import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager } from '@nitrots import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; import { Base, BaseProps } from '../Base'; +const AVATAR_CACHE_MAX_SIZE = 200; const AVATAR_IMAGE_CACHE: Map = new Map(); export interface LayoutAvatarImageViewProps extends BaseProps @@ -75,7 +76,16 @@ export const LayoutAvatarImageView: FC = props => if(imageUrl && !isDisposed.current) { - if(!avatarImage.isPlaceholder()) AVATAR_IMAGE_CACHE.set(figureKey, imageUrl); + if(!avatarImage.isPlaceholder()) + { + if(AVATAR_IMAGE_CACHE.size >= AVATAR_CACHE_MAX_SIZE) + { + const firstKey = AVATAR_IMAGE_CACHE.keys().next().value; + AVATAR_IMAGE_CACHE.delete(firstKey); + } + + AVATAR_IMAGE_CACHE.set(figureKey, imageUrl); + } setAvatarUrl(imageUrl); } diff --git a/src/common/layout/LayoutRoomObjectImageView.tsx b/src/common/layout/LayoutRoomObjectImageView.tsx index 01328af..aa9301c 100644 --- a/src/common/layout/LayoutRoomObjectImageView.tsx +++ b/src/common/layout/LayoutRoomObjectImageView.tsx @@ -1,5 +1,5 @@ import { GetRoomEngine, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; -import { CSSProperties, FC, useEffect, useMemo, useState } from 'react'; +import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; import { Base, BaseProps } from '../Base'; interface LayoutRoomObjectImageViewProps extends BaseProps @@ -15,6 +15,14 @@ export const LayoutRoomObjectImageView: FC = pro { const { roomId = -1, objectId = 1, category = -1, direction = 2, scale = 1, style = {}, ...rest } = props; const [ imageElement, setImageElement ] = useState(null); + const isMounted = useRef(true); + + useEffect(() => + { + isMounted.current = true; + + return () => { isMounted.current = false; }; + }, []); const getStyle = useMemo(() => { @@ -42,15 +50,23 @@ export const LayoutRoomObjectImageView: FC = pro useEffect(() => { const imageResult = GetRoomEngine().getRoomObjectImage(roomId, objectId, category, new Vector3d(direction * 45), 64, { - imageReady: async (id, texture, image) => setImageElement(await TextureUtils.generateImage(texture)), + imageReady: async (id, texture, image) => + { + const img = await TextureUtils.generateImage(texture); + + if(img && isMounted.current) setImageElement(img); + }, imageFailed: null }); - // needs (roomObjectImage.data.width > 140) || (roomObjectImage.data.height > 200) scale 1 - if(!imageResult) return; - (async () => setImageElement(await TextureUtils.generateImage(imageResult.data)))(); + (async () => + { + const img = await TextureUtils.generateImage(imageResult.data); + + if(img && isMounted.current) setImageElement(img); + })(); }, [ roomId, objectId, category, direction, scale ]); if(!imageElement) return null; diff --git a/src/common/layout/LayoutRoomPreviewerView.tsx b/src/common/layout/LayoutRoomPreviewerView.tsx index ca1ef26..d35294c 100644 --- a/src/common/layout/LayoutRoomPreviewerView.tsx +++ b/src/common/layout/LayoutRoomPreviewerView.tsx @@ -38,10 +38,11 @@ export const LayoutRoomPreviewerView: FC<{ clear: true }); - let canvas = GetRenderer().texture.generateCanvas(texture); + const canvas = GetRenderer().texture.generateCanvas(texture); const base64 = canvas.toDataURL('image/png'); - canvas = null; + canvas.width = 0; + canvas.height = 0; elementRef.current.style.backgroundImage = `url(${ base64 })`; }; diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index 19a39bb..bb1cedf 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -1,7 +1,7 @@ import { AvatarEditorFigureCategory, AvatarFigureContainer, AvatarFigurePartType, FigureSetIdsMessageEvent, GetAvatarRenderManager, GetSessionDataManager, GetWardrobeMessageComposer, IAvatarFigureContainer, IFigurePartSet, IPalette, IPartColor, SetType, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useBetween } from 'use-between'; -import { AvatarEditorColorSorter, AvatarEditorPartSorter, AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } from '../../api'; +import { AvatarEditorColorSorter, AvatarEditorPartSorter, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategory, IAvatarEditorCategoryPartItem, Randomizer, SendMessageComposer } from '../../api'; import { useMessageEvent } from '../events'; import { useFigureData } from './useFigureData'; @@ -244,11 +244,6 @@ const useAvatarEditorState = () => setSavedFigures(savedFigures); }); - useEffect(() => - { - AvatarEditorThumbnailsHelper.clearCache(); - }, [ selectedColorParts ]); - useEffect(() => { if(!isVisible) return;