From a59c093e0480455a0a7db60b4150f904554cfadd Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 1 May 2025 14:13:06 +0200 Subject: [PATCH] :up: First Stage ! --- src/App.scss | 2 +- src/api/GetUIVersion.ts | 2 +- src/common/layout/LayoutAvatarImageView.tsx | 148 +++++++++--- src/common/layout/LayoutBadgeImageView.tsx | 82 +++++-- src/common/layout/LayoutFurniImageView.tsx | 123 ++++++++-- src/common/layout/LayoutPetImageView.tsx | 223 +++++++++++++----- src/common/layout/LayoutRoomPreviewerView.tsx | 94 +++++--- .../avatar-editor/AvatarEditorView.scss | 10 +- .../AvatarEditorFigureSetItemView.tsx | 95 +++++++- .../camera/views/CameraWidgetCaptureView.tsx | 50 ++-- .../camera/views/CameraWidgetCheckoutView.tsx | 20 +- .../views/CameraWidgetShowPhotoView.tsx | 19 +- .../views/editor/CameraWidgetEditorView.tsx | 123 ++++++++-- .../page/common/CatalogGridOfferView.tsx | 2 +- .../views/bot/InventoryBotItemView.tsx | 85 +++---- .../NotificationCenterView.scss | 36 ++- .../alert-layouts/NitroSystemAlertView.tsx | 47 ++-- .../infostand/InfoStandWidgetFurniView.tsx | 215 +++++++++-------- .../infostand/InfoStandWidgetUserView.tsx | 2 +- src/components/toolbar/ToolbarMeView.tsx | 2 +- submodules/renderer/package.json | 61 ++--- .../src/api/nitro/avatar/IAvatarImage.ts | 4 +- submodules/renderer/src/core/NitroVersion.ts | 2 +- submodules/renderer/src/nitro/Plugins.ts | 12 +- .../renderer/src/nitro/avatar/AvatarImage.ts | 57 +++-- .../camera/RenderRoomMessageComposer.ts | 14 +- .../FurnitureChangeStateWhenStepOnLogic.ts | 2 +- .../FurnitureDynamicThumbnailVisualization.ts | 54 +++-- .../IsometricImageFurniVisualization.ts | 163 ++++++++----- .../src/pixi-proxy/RoomTextureUtils.ts | 43 ++-- .../renderer/src/pixi-proxy/TextureUtils.ts | 138 +++++++++-- 31 files changed, 1335 insertions(+), 595 deletions(-) diff --git a/src/App.scss b/src/App.scss index 9b05e7b..0563da2 100644 --- a/src/App.scss +++ b/src/App.scss @@ -21,7 +21,7 @@ $toolbar-height: 55px; $achievement-width: 375px; $achievement-height: 405px; -$avatar-editor-width: 520px; +$avatar-editor-width: 545px; $avatar-editor-height: 553px; $backgrounds-width: 534px; diff --git a/src/api/GetUIVersion.ts b/src/api/GetUIVersion.ts index cf8d5a5..3b149ec 100644 --- a/src/api/GetUIVersion.ts +++ b/src/api/GetUIVersion.ts @@ -1 +1 @@ -export const GetUIVersion = () => '2.1.1'; +export const GetUIVersion = () => '2.2.5'; diff --git a/src/common/layout/LayoutAvatarImageView.tsx b/src/common/layout/LayoutAvatarImageView.tsx index d127bd9..d56cd31 100644 --- a/src/common/layout/LayoutAvatarImageView.tsx +++ b/src/common/layout/LayoutAvatarImageView.tsx @@ -3,6 +3,9 @@ import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; import { GetAvatarRenderManager } from '../../api'; import { Base, BaseProps } from '../Base'; +// Cache for avatar image URLs +const AVATAR_IMAGE_CACHE: Map = new Map(); + export interface LayoutAvatarImageViewProps extends BaseProps { figure: string; @@ -15,75 +18,166 @@ export interface LayoutAvatarImageViewProps extends BaseProps export const LayoutAvatarImageView: FC = props => { const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; - const [ avatarUrl, setAvatarUrl ] = useState(null); - const [ randomValue, setRandomValue ] = useState(-1); + const [ avatarUrl, setAvatarUrl ] = useState(null); + const [ isReady, setIsReady ] = useState(false); const isDisposed = useRef(false); + const retryCount = useRef(0); + const maxRetries = 3; + const elementRef = useRef(null); + + const figureKey = useMemo(() => [ figure, gender, direction, headOnly ].join('-'), [ figure, gender, direction, headOnly ]); const getClassNames = useMemo(() => { const newClassNames: string[] = [ 'avatar-image' ]; + if(headOnly) newClassNames.push('head-only'); + if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames ]); + }, [ classNames, headOnly ]); const getStyle = useMemo(() => { let newStyle: CSSProperties = {}; - if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`; + if(avatarUrl && avatarUrl.length) + { + newStyle.backgroundImage = `url('${ avatarUrl }')`; + } + + newStyle.backgroundRepeat = 'no-repeat'; + newStyle.backgroundPosition = headOnly ? 'center -8px' : 'center'; + newStyle.position = 'relative'; if(scale !== 1) { newStyle.transform = `scale(${ scale })`; - if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; } if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; return newStyle; - }, [ avatarUrl, scale, style ]); + }, [ avatarUrl, scale, style, headOnly ]); - useEffect(() => + const loadAvatarImage = async () => { + if(isDisposed.current) return; + + if(!figure || figure.length === 0) + { + setAvatarUrl(null); + return; + } + + const cached = AVATAR_IMAGE_CACHE.get(figureKey); + if(cached) + { + setAvatarUrl(cached.url); + retryCount.current = 0; + return; + } + const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, { - resetFigure: figure => + resetFigure: figure => { if(isDisposed.current) return; - - setRandomValue(Math.random()); + loadAvatarImage(); }, - dispose: () => - {}, + dispose: () => {}, disposed: false }, null); - if(!avatarImage) return; - - let setType = AvatarSetType.FULL; + if(!avatarImage) + { + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + setTimeout(loadAvatarImage, 1000); + } + else + { + setAvatarUrl(null); + } + return; + } - if(headOnly) setType = AvatarSetType.HEAD; + try + { + const setType = headOnly ? AvatarSetType.HEAD : AvatarSetType.FULL; + avatarImage.setDirection(setType, direction); - avatarImage.setDirection(setType, direction); + const image = await avatarImage.getCroppedImage(setType); - const image = avatarImage.getCroppedImage(setType); + if(isDisposed.current) return; - if(image) setAvatarUrl(image.src); - - avatarImage.dispose(); - }, [ figure, gender, direction, headOnly, randomValue ]); + if(image && image.src && typeof image.src === 'string' && image.src.startsWith('data:image/')) + { + setAvatarUrl(image.src); + AVATAR_IMAGE_CACHE.set(figureKey, { url: image.src }); + retryCount.current = 0; + } + else + { + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + setTimeout(loadAvatarImage, 1000); + } + else + { + setAvatarUrl(null); + } + } + } + catch(error) + { + console.warn(`LayoutAvatarImageView: Error loading avatar image`, error); + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + setTimeout(loadAvatarImage, 1000); + } + else + { + setAvatarUrl(null); + } + } + finally + { + setTimeout(() => { + if(!isDisposed.current) avatarImage.dispose(); + }, 500); + } + }; useEffect(() => { isDisposed.current = false; + retryCount.current = 0; + setIsReady(true); + + loadAvatarImage(); + + const handleVisibilityChange = () => + { + if(document.visibilityState === 'visible') + { + setAvatarUrl(null); + loadAvatarImage(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); return () => { isDisposed.current = true; - } - }, []); - - return ; -} + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [ figure, gender, direction, headOnly, figureKey ]); + + return ; +}; \ No newline at end of file diff --git a/src/common/layout/LayoutBadgeImageView.tsx b/src/common/layout/LayoutBadgeImageView.tsx index 3c0979d..11a230f 100644 --- a/src/common/layout/LayoutBadgeImageView.tsx +++ b/src/common/layout/LayoutBadgeImageView.tsx @@ -23,9 +23,7 @@ export const LayoutBadgeImageView: FC = props => const newClassNames: string[] = [ 'badge-image' ]; if(isGroup) newClassNames.push('group-badge'); - if(isGrayscale) newClassNames.push('grayscale'); - if(classNames.length) newClassNames.push(...classNames); return newClassNames; @@ -37,16 +35,16 @@ export const LayoutBadgeImageView: FC = props => if(imageElement) { - newStyle.backgroundImage = `url(${ (isGroup) ? imageElement.src : GetConfiguration('badge.asset.url').replace('%badgename%', badgeCode.toString())})`; + const badgeUrl = isGroup ? imageElement.src : GetConfiguration('badge.asset.url', '').replace('%badgename%', badgeCode.toString()); + + newStyle.backgroundImage = `url(${ badgeUrl })`; newStyle.width = imageElement.width; newStyle.height = imageElement.height; if(scale !== 1) { newStyle.transform = `scale(${ scale })`; - if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; - newStyle.width = (imageElement.width * scale); newStyle.height = (imageElement.height * scale); } @@ -55,37 +53,81 @@ export const LayoutBadgeImageView: FC = props => if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; return newStyle; - }, [ imageElement, scale, style ]); + }, [ badgeCode, imageElement, isGroup, scale, style ]); useEffect(() => { - if(!badgeCode || !badgeCode.length) return; + + if(!badgeCode || !badgeCode.length) + { + console.warn('LayoutBadgeImageView: Invalid or empty badgeCode', badgeCode); + setImageElement(null); + return; + } let didSetBadge = false; - const onBadgeImageReadyEvent = (event: BadgeImageReadyEvent) => + const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) => { if(event.badgeId !== badgeCode) return; - const element = TextureUtils.generateImage(new NitroSprite(event.image)); + try + { + const sprite = new NitroSprite(event.image); + const element = await TextureUtils.generateImage(sprite); - element.onload = () => setImageElement(element); - - didSetBadge = true; + if(element && element.src && element.src.startsWith('data:image/')) + { + setImageElement(element); + didSetBadge = true; + } + else + { + console.warn('LayoutBadgeImageView: Invalid badge image (event)', element); + } + } + catch(error) + { + console.warn('LayoutBadgeImageView: Error generating badge image (event)', error); + } GetSessionDataManager().events.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent); - } + }; GetSessionDataManager().events.addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent); - const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode); - - if(texture && !didSetBadge) + const loadBadgeImage = async () => { - const element = TextureUtils.generateImage(new NitroSprite(texture)); + const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode); - element.onload = () => setImageElement(element); - } + if(texture && !didSetBadge) + { + try + { + const sprite = new NitroSprite(texture); + const element = await TextureUtils.generateImage(sprite); + + if(element && element.src && element.src.startsWith('data:image/')) + { + setImageElement(element); + } + else + { + console.warn('LayoutBadgeImageView: Invalid badge image (direct)', element); + } + } + catch(error) + { + console.warn('LayoutBadgeImageView: Error generating badge image (direct)', error); + } + } + else + { + console.log('LayoutBadgeImageView: No texture found for badge', badgeCode); + } + }; + + loadBadgeImage(); return () => GetSessionDataManager().events.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent); }, [ badgeCode, isGroup ]); @@ -100,4 +142,4 @@ export const LayoutBadgeImageView: FC = props => { children } ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/common/layout/LayoutFurniImageView.tsx b/src/common/layout/LayoutFurniImageView.tsx index ef8696c..b2adc68 100644 --- a/src/common/layout/LayoutFurniImageView.tsx +++ b/src/common/layout/LayoutFurniImageView.tsx @@ -15,7 +15,7 @@ interface LayoutFurniImageViewProps extends BaseProps export const LayoutFurniImageView: FC = props => { - const { productType = 's', productClassId = -1, direction = 2, extraData = '', scale = 1, style = {}, ...rest } = props; + const { productType = 's', productClassId = -1, direction = 2, extraData = '', scale = 1, style = {}, classNames = [], ...rest } = props; const [ imageElement, setImageElement ] = useState(null); const getStyle = useMemo(() => @@ -24,15 +24,19 @@ export const LayoutFurniImageView: FC = props => if(imageElement?.src?.length) { + console.log('LayoutFurniImageView: Applying image URL', imageElement.src); newStyle.backgroundImage = `url('${ imageElement.src }')`; newStyle.width = imageElement.width; newStyle.height = imageElement.height; } + else + { + console.log('LayoutFurniImageView: No imageElement, skipping style'); + } if(scale !== 1) { newStyle.transform = `scale(${ scale })`; - if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; } @@ -43,40 +47,111 @@ export const LayoutFurniImageView: FC = props => useEffect(() => { + console.log('LayoutFurniImageView: productType:', productType, 'productClassId:', productClassId, 'direction:', direction, 'extraData:', extraData); + + if(productClassId < 0 || !productType) + { + console.warn('LayoutFurniImageView: Invalid productClassId or productType', { productClassId, productType }); + setImageElement(null); + return; + } + let imageResult: ImageResult = null; const listener: IGetImageListener = { - imageReady: (id, texture, image) => + imageReady: async (id, texture, image) => { - if(!image && texture) - { - image = TextureUtils.generateImage(texture); - } + console.log('LayoutFurniImageView: imageReady called', { id, texture, image }); - image.onload = () => setImageElement(image); + try + { + if(!image && texture) + { + image = await TextureUtils.generateImage(texture); + console.log('LayoutFurniImageView: Generated image from texture', image?.src); + } + + if(image && image.src && image.src.startsWith('data:image/')) + { + image.onload = () => { + console.log('LayoutFurniImageView: Image loaded', image.src); + setImageElement(image); + }; + if(image.complete) { + console.log('LayoutFurniImageView: Image already complete', image.src); + setImageElement(image); + } + } + else + { + console.warn('LayoutFurniImageView: Invalid image in imageReady', image); + setImageElement(null); + } + } + catch(error) + { + console.warn('LayoutFurniImageView: Error in imageReady', error); + setImageElement(null); + } }, - imageFailed: null + imageFailed: (id) => { + console.warn('LayoutFurniImageView: Image fetch failed for id', id); + setImageElement(null); + } }; - switch(productType.toLocaleLowerCase()) + try { - case ProductTypeEnum.FLOOR: - imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); - break; - case ProductTypeEnum.WALL: - imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); - break; + switch(productType.toLowerCase()) + { + case ProductTypeEnum.FLOOR: + console.log('LayoutFurniImageView: Fetching floor furniture image'); + imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); + break; + case ProductTypeEnum.WALL: + console.log('LayoutFurniImageView: Fetching wall furniture image'); + imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); + break; + default: + console.warn('LayoutFurniImageView: Unknown productType', productType); + setImageElement(null); + return; + } + + if(imageResult) + { + const image = imageResult.getImage(); + console.log('LayoutFurniImageView: Immediate imageResult', image?.src); + + if(image && image.src && image.src.startsWith('data:image/')) + { + image.onload = () => { + console.log('LayoutFurniImageView: Immediate image loaded', image.src); + setImageElement(image); + }; + if(image.complete) { + console.log('LayoutFurniImageView: Immediate image already complete', image.src); + setImageElement(image); + } + } + } + else + { + console.warn('LayoutFurniImageView: No imageResult returned'); + } } - - if(imageResult) + catch(error) { - const image = imageResult.getImage(); - - image.onload = () => setImageElement(image); + console.warn('LayoutFurniImageView: Error fetching image', error); + setImageElement(null); } }, [ productType, productClassId, direction, extraData ]); - if(!imageElement) return null; + if(!imageElement) + { + console.log('LayoutFurniImageView: Skipping render, no imageElement'); + return null; + } - return ; -} + return ; +}; \ No newline at end of file diff --git a/src/common/layout/LayoutPetImageView.tsx b/src/common/layout/LayoutPetImageView.tsx index 27d43de..af1861b 100644 --- a/src/common/layout/LayoutPetImageView.tsx +++ b/src/common/layout/LayoutPetImageView.tsx @@ -14,41 +14,44 @@ interface LayoutPetImageViewProps extends BaseProps headOnly?: boolean; direction?: number; scale?: number; + isIcon?: boolean; } export const LayoutPetImageView: FC = props => { - const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, style = {}, ...rest } = props; + const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, isIcon = false, style = {}, classNames = [], ...rest } = props; const [ petUrl, setPetUrl ] = useState(null); const [ width, setWidth ] = useState(0); const [ height, setHeight ] = useState(0); const isDisposed = useRef(false); + const retryCount = useRef(0); + const maxRetries = 3; + const prevFigure = useRef(''); const getStyle = useMemo(() => { let newStyle: CSSProperties = {}; - if(petUrl && petUrl.length) newStyle.backgroundImage = `url(${ petUrl })`; + if(petUrl && petUrl.length) + { + newStyle.backgroundImage = `url(${ petUrl })`; + newStyle.width = width; + newStyle.height = height; + } if(scale !== 1) { newStyle.transform = `scale(${ scale })`; - if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; } - newStyle.width = width; - newStyle.height = height; - if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; return newStyle; - }, [ petUrl, scale, style, width, height ]); + }, [ petUrl, scale, style, width, height, isIcon ]); - useEffect(() => + const fetchPetImage = () => { - let url = null; - let petTypeId = typeId; let petPaletteId = paletteId; let petColor1 = petColor; @@ -57,65 +60,177 @@ export const LayoutPetImageView: FC = props => if(figure && figure.length) { - const petFigureData = new PetFigureData(figure); - - petTypeId = petFigureData.typeId; - petPaletteId = petFigureData.paletteId; - petColor1 = petFigureData.color; - petCustomParts = petFigureData.customParts; + try + { + const petFigureData = new PetFigureData(figure); + petTypeId = petFigureData.typeId; + petPaletteId = petFigureData.paletteId; + petColor1 = petFigureData.color; + petCustomParts = petFigureData.customParts; + } + catch(error) + { + console.warn(`LayoutPetImageView: Error parsing PetFigureData (isIcon: ${isIcon})`, error, 'Figure:', figure); + setPetUrl(null); + return; + } } if(petTypeId === 16) petHeadOnly = false; - const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, { - imageReady: (id, texture, image) => - { - if(isDisposed.current) return; - - if(image) - { - setPetUrl(image.src); - setWidth(image.width); - setHeight(image.height); - } - - else if(texture) - { - setPetUrl(TextureUtils.generateImageUrl(texture)); - setWidth(texture.width); - setHeight(texture.height); - } - }, - imageFailed: (id) => - { - - } - }, petHeadOnly, 0, petCustomParts, posture); - - if(imageResult) + if(petTypeId < 0 || petPaletteId < 0) { - const image = imageResult.getImage(); + console.warn(`LayoutPetImageView: Invalid petTypeId or petPaletteId (isIcon: ${isIcon})`, { petTypeId, petPaletteId, figure }); + setPetUrl(null); + return; + } - if(image) + try + { + const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), isIcon ? 32 : 64, { + imageReady: async (id, texture, image) => + { + if(isDisposed.current) return; + + try + { + if(image && image.src && image.src.startsWith('data:image/')) + { + image.onload = () => { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + }; + if(image.complete) + { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + } + } + else if(texture) + { + const url = await TextureUtils.generateImageUrl(texture); + if(url && url.startsWith('data:image/')) + { + setPetUrl(url); + setWidth(texture.width || (isIcon ? 32 : 64)); + setHeight(texture.height || (isIcon ? 32 : 64)); + } + else + { + console.warn(`LayoutPetImageView: Invalid texture URL (isIcon: ${isIcon})`, url); + setPetUrl(null); + } + } + else + { + console.warn(`LayoutPetImageView: No image or texture in imageReady (isIcon: ${isIcon})`, { id }); + setPetUrl(null); + } + } + catch(error) + { + console.warn(`LayoutPetImageView: Error in imageReady (isIcon: ${isIcon})`, error, 'ID:', id); + setPetUrl(null); + } + }, + imageFailed: (id) => + { + console.warn(`LayoutPetImageView: Image fetch failed for id (isIcon: ${isIcon})`, id, 'Props:', { petTypeId, petPaletteId, posture }); + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`); + setTimeout(fetchPetImage, 1000); + } + else + { + setPetUrl(null); + } + } + }, petHeadOnly, 0, petCustomParts, posture); + + if(imageResult) { - setPetUrl(image.src); - setWidth(image.width); - setHeight(image.height); + (async () => + { + const image = await imageResult.getImage(); + + if(image && image.src && image.src.startsWith('data:image/')) + { + image.onload = () => { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + }; + if(image.complete) + { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + } + } + })(); + } + else + { + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`); + setTimeout(fetchPetImage, 1000); + } } } - }, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]); + catch(error) + { + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`); + setTimeout(fetchPetImage, 1000); + } + else + { + setPetUrl(null); + } + } + }; useEffect(() => { isDisposed.current = false; + retryCount.current = 0; + + // Force reload if figure changes + const figureChanged = prevFigure.current !== figure; + if(figureChanged) + { + setPetUrl(null); + prevFigure.current = figure; + } + + fetchPetImage(); + + // Handle visibility changes + const handleVisibilityChange = () => + { + if(document.visibilityState === 'visible') + { + setPetUrl(null); // Reset to force reload + fetchPetImage(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); return () => { isDisposed.current = true; - } - }, []); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction, isIcon ]); - const url = `url('${ petUrl }')`; - - return ; -} + return ; +}; \ No newline at end of file diff --git a/src/common/layout/LayoutRoomPreviewerView.tsx b/src/common/layout/LayoutRoomPreviewerView.tsx index 69fbea8..2e85624 100644 --- a/src/common/layout/LayoutRoomPreviewerView.tsx +++ b/src/common/layout/LayoutRoomPreviewerView.tsx @@ -24,43 +24,79 @@ export const LayoutRoomPreviewerView: FC = props = useEffect(() => { - if(!roomPreviewer) return; + if(!roomPreviewer || height <= 0) + { + return; + } - const update = (time: number) => + const update = async (time: number) => { if(!roomPreviewer || !renderingCanvas || !elementRef.current) return; - - roomPreviewer.updatePreviewRoomView(); - if(!renderingCanvas.canvasUpdated) return; + try + { + roomPreviewer.updatePreviewRoomView(); - elementRef.current.style.backgroundImage = `url(${ TextureUtils.generateImageUrl(renderingCanvas.master) })`; + if(!renderingCanvas.canvasUpdated) return; + + const imageUrl = await TextureUtils.generateImageUrl(renderingCanvas.master); + + if(imageUrl && imageUrl.startsWith('data:image/')) + { + elementRef.current.style.backgroundImage = `url(${ imageUrl })`; + } + else + { + console.warn('LayoutRoomPreviewerView: Invalid image URL', imageUrl); + elementRef.current.style.backgroundImage = ''; + } + } + catch(error) + { + console.warn('LayoutRoomPreviewerView: Error updating preview', error); + elementRef.current.style.backgroundImage = ''; + } } if(!renderingCanvas) { if(elementRef.current && roomPreviewer) { - const computed = document.defaultView.getComputedStyle(elementRef.current, null); + try + { + const computed = document.defaultView.getComputedStyle(elementRef.current, null); + let backgroundColor = computed.backgroundColor; - let backgroundColor = computed.backgroundColor; + backgroundColor = ColorConverter.rgbStringToHex(backgroundColor); + backgroundColor = backgroundColor.replace('#', '0x'); - backgroundColor = ColorConverter.rgbStringToHex(backgroundColor); - backgroundColor = backgroundColor.replace('#', '0x'); + roomPreviewer.backgroundColor = parseInt(backgroundColor, 16); - roomPreviewer.backgroundColor = parseInt(backgroundColor, 16); + const width = elementRef.current.parentElement.clientWidth; - const width = elementRef.current.parentElement.clientWidth; - - roomPreviewer.getRoomCanvas(width, height); + roomPreviewer.getRoomCanvas(width, height); - const canvas = roomPreviewer.getRenderingCanvas(); + const canvas = roomPreviewer.getRenderingCanvas(); - setRenderingCanvas(canvas); - - canvas.canvasUpdated = true; - - update(-1); + if(canvas) + { + setRenderingCanvas(canvas); + canvas.canvasUpdated = true; + update(-1); + } + else + { + console.warn('LayoutRoomPreviewerView: Failed to initialize canvas'); + } + } + catch(error) + { + console.warn('LayoutRoomPreviewerView: Error initializing canvas', error); + } + } + else + { + console.warn('LayoutRoomPreviewerView: Missing elementRef or roomPreviewer'); } } @@ -72,20 +108,24 @@ export const LayoutRoomPreviewerView: FC = props = const width = elementRef.current.parentElement.offsetWidth; - roomPreviewer.modifyRoomCanvas(width, height); - - update(-1); + try + { + roomPreviewer.modifyRoomCanvas(width, height); + update(-1); + } + catch(error) + { + console.warn('LayoutRoomPreviewerView: Error resizing canvas', error); + } }); - + resizeObserver.observe(elementRef.current); return () => { resizeObserver.disconnect(); - GetTicker().remove(update); } - }, [ renderingCanvas, roomPreviewer, elementRef, height ]); return ( @@ -94,4 +134,4 @@ export const LayoutRoomPreviewerView: FC = props = { children } ); -} +}; \ No newline at end of file diff --git a/src/components/avatar-editor/AvatarEditorView.scss b/src/components/avatar-editor/AvatarEditorView.scss index 160bd17..b822536 100644 --- a/src/components/avatar-editor/AvatarEditorView.scss +++ b/src/components/avatar-editor/AvatarEditorView.scss @@ -334,7 +334,7 @@ position: absolute; left: 0; right: 0; - bottom: 125px; + bottom: -125px; margin: 0 auto; z-index: 4; } @@ -359,7 +359,7 @@ } .choose-clothing { - width: 320px; + width: 360px; } .color-picker-frame { @@ -477,9 +477,9 @@ .avatar-parts { border: none !important; - height: 42px; - width: 42px; - background-position: center; + height: 50px; + width: 50px; + background-position: center; background-repeat: no-repeat; border-radius: 2rem !important; overflow: visible !important; diff --git a/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx index e960413..c433244 100644 --- a/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,6 +1,6 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useState, useRef } from 'react'; import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api'; -import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common'; +import { LayoutGridItem, LayoutGridItemProps } from '../../../../common'; import { AvatarEditorIcon } from '../AvatarEditorIcon'; export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps @@ -12,21 +12,104 @@ export const AvatarEditorFigureSetItemView: FC(null); + const [ isValid, setIsValid ] = useState(true); + const isDisposed = useRef(false); + const retryCount = useRef(0); + const maxRetries = 1; const hcDisabled = GetConfiguration('hc.disabled', false); + const loadPartImage = async () => + { + if(isDisposed.current) return; + + if(!partItem) + { + setImageUrl(null); + setIsValid(false); + return; + } + + let resolvedImageUrl: string | null = null; + + if(partItem.imageUrl && typeof partItem.imageUrl === 'object' && 'then' in partItem.imageUrl) + { + try + { + resolvedImageUrl = await partItem.imageUrl; + } + catch(error) + { + console.warn(`AvatarEditorFigureSetItemView: Failed to resolve imageUrl promise for item ${partItem.id}`, error); + } + } + else if(typeof partItem.imageUrl === 'string') + { + resolvedImageUrl = partItem.imageUrl; + } + + if(!resolvedImageUrl || !resolvedImageUrl.startsWith('data:image/')) + { + if(retryCount.current < maxRetries) + { + retryCount.current += 1; + setTimeout(loadPartImage, 1500); + } + else + { + setImageUrl(null); + setIsValid(false); + } + return; + } + + setImageUrl(resolvedImageUrl); + setIsValid(true); + retryCount.current = 0; + }; + useEffect(() => { - const rerender = () => setUpdateId(prevValue => (prevValue + 1)); + isDisposed.current = false; + retryCount.current = 0; + setIsValid(true); + + loadPartImage(); + + const rerender = () => + { + setUpdateId(prevValue => (prevValue + 1)); + loadPartImage(); + }; partItem.notify = rerender; - return () => partItem.notify = null; + const handleVisibilityChange = () => + { + if(document.visibilityState === 'visible') + { + setImageUrl(null); + setIsValid(true); + loadPartImage(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => + { + isDisposed.current = true; + partItem.notify = null; + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; }, [ partItem ]); + if(!isValid) return null; + return (
- + { !hcDisabled && partItem.isHC && } { partItem.isClear && } { partItem.isSellable && } @@ -34,4 +117,4 @@ export const AvatarEditorFigureSetItemView: FC
); -} +}; \ No newline at end of file diff --git a/src/components/camera/views/CameraWidgetCaptureView.tsx b/src/components/camera/views/CameraWidgetCaptureView.tsx index 43f8230..7b8750e 100644 --- a/src/components/camera/views/CameraWidgetCaptureView.tsx +++ b/src/components/camera/views/CameraWidgetCaptureView.tsx @@ -1,5 +1,5 @@ import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer'; -import { FC, useRef } from 'react'; +import { FC, useRef, useState } from 'react'; import { FaTimes } from 'react-icons/fa'; import { CameraPicture, CreateLinkEvent, GetRoomEngine, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api'; import { Column, DraggableWindowCamera, Flex } from '../../../common'; @@ -20,6 +20,7 @@ export const CameraWidgetCaptureView: FC = props = const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera(); const { simpleAlert = null } = useNotification(); const elementRef = useRef(); + const [ isCapturing, setIsCapturing ] = useState(false); const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null); @@ -32,7 +33,7 @@ export const CameraWidgetCaptureView: FC = props = return new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height)); } - const takePicture = () => + const takePicture = async () => { if(selectedPictureIndex > -1) { @@ -40,21 +41,38 @@ export const CameraWidgetCaptureView: FC = props = return; } - const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds()); - - const clone = [ ...cameraRoll ]; - - if(clone.length >= CAMERA_ROLL_LIMIT) + setIsCapturing(true); + try { - simpleAlert(LocalizeText('camera.full.body')); + const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds()); + const imageUrl = await TextureUtils.generateImageUrl(texture); - clone.pop(); + if (!imageUrl || typeof imageUrl !== 'string' || !imageUrl.startsWith('data:image/')) { + simpleAlert(LocalizeText('camera.error.body')); + return; + } + + const clone = [ ...cameraRoll ]; + + if(clone.length >= CAMERA_ROLL_LIMIT) + { + simpleAlert(LocalizeText('camera.full.body')); + clone.pop(); + } + + PlaySound(SoundNames.CAMERA_SHUTTER); + clone.push(new CameraPicture(texture, imageUrl)); + + setCameraRoll(clone); + } + catch (error) + { + simpleAlert(LocalizeText('camera.error.body')); + } + finally + { + setIsCapturing(false); } - - PlaySound(SoundNames.CAMERA_SHUTTER); - clone.push(new CameraPicture(texture, TextureUtils.generateImageUrl(texture))); - - setCameraRoll(clone); } return ( @@ -62,7 +80,7 @@ export const CameraWidgetCaptureView: FC = props = { selectedPicture && }
-
CreateLinkEvent('habbopages/camera') }>
+
CreateLinkEvent('habbopages/camera') }>
@@ -88,4 +106,4 @@ export const CameraWidgetCaptureView: FC = props = ); -} +} \ No newline at end of file diff --git a/src/components/camera/views/CameraWidgetCheckoutView.tsx b/src/components/camera/views/CameraWidgetCheckoutView.tsx index 23d581b..0ed3719 100644 --- a/src/components/camera/views/CameraWidgetCheckoutView.tsx +++ b/src/components/camera/views/CameraWidgetCheckoutView.tsx @@ -46,15 +46,17 @@ export const CameraWidgetCheckoutView: FC = props useMessageEvent(CameraStorageUrlMessageEvent, event => { const parser = event.getParser(); + const cameraUrl = GetConfiguration('camera.url'); + const fullUrl = cameraUrl + '/' + parser.url; - setPictureUrl(GetConfiguration('camera.url') + '/' + parser.url); + setPictureUrl(fullUrl); }); useMessageEvent(NotEnoughBalanceMessageEvent, event => { const parser = event.getParser(); - if (!parser) return null; + if (!parser) return; if (parser.notEnoughCredits && !parser.notEnoughActivityPoints) simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), null, null, null, LocalizeText('catalog.alert.notenough.title')); @@ -91,7 +93,6 @@ export const CameraWidgetCheckoutView: FC = props useEffect(() => { if(!base64Url) return; - GetRoomEngine().saveBase64AsScreenshot(base64Url); }, [ base64Url ]); @@ -102,12 +103,15 @@ export const CameraWidgetCheckoutView: FC = props processAction('close') } /> - { (pictureUrl && pictureUrl.length) && - } - { (!pictureUrl || !pictureUrl.length) && + { (pictureUrl && pictureUrl.length) ? ( + + ) : base64Url ? ( + + ) : ( { LocalizeText('camera.loading') } - } + + ) } @@ -172,4 +176,4 @@ export const CameraWidgetCheckoutView: FC = props ); -} +} \ No newline at end of file diff --git a/src/components/camera/views/CameraWidgetShowPhotoView.tsx b/src/components/camera/views/CameraWidgetShowPhotoView.tsx index c01ea07..5b99de8 100644 --- a/src/components/camera/views/CameraWidgetShowPhotoView.tsx +++ b/src/components/camera/views/CameraWidgetShowPhotoView.tsx @@ -36,26 +36,31 @@ export const CameraWidgetShowPhotoView: FC = pro }); } - const getUserData = (roomId: number, objectId: number, type: string): number | string => + const getUserData = (roomId: number, objectId: number, type: string): number | string => { const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL); - if (!roomObject) return; - return type == 'username' ? roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + if (!roomObject) return ''; + return type === 'username' ? roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); } - useEffect(() => { setImageIndex(currentIndex); }, [ currentIndex ]); + useEffect(() => + { + setImageIndex(currentIndex); + }, [ currentIndex, currentPhotos ]); if(!currentImage) return null; + const imageUrl = currentImage.w || ''; + return ( - - { !currentImage.w && { LocalizeText('camera.loading') } } + + { !imageUrl && { LocalizeText('camera.loading') } } { currentImage.m && currentImage.m.length && { currentImage.m } } { new Date(currentImage.t * 1000).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' }) } - GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } + GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') || 'Unknown' } { (currentPhotos.length > 1) && diff --git a/src/components/camera/views/editor/CameraWidgetEditorView.tsx b/src/components/camera/views/editor/CameraWidgetEditorView.tsx index 3eee17b..8a219ae 100644 --- a/src/components/camera/views/editor/CameraWidgetEditorView.tsx +++ b/src/components/camera/views/editor/CameraWidgetEditorView.tsx @@ -1,10 +1,11 @@ -import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer'; +import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect, TextureUtils, NitroSprite } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa'; import ReactSlider from 'react-slider'; import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, GetRoomCameraWidgetManager, LocalizeText } from '../../../../api'; import { Button, ButtonGroup, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common'; import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView'; +import { ColorMatrixFilter } from '@pixi/filter-color-matrix'; export interface CameraWidgetEditorViewProps { @@ -26,6 +27,7 @@ export const CameraWidgetEditorView: FC = props => const [ selectedEffects, setSelectedEffects ] = useState([]); const [ effectsThumbnails, setEffectsThumbnails ] = useState([]); const [ isZoomed, setIsZoomed ] = useState(false); + const [ currentPictureUrl, setCurrentPictureUrl ] = useState(null); const getColorMatrixEffects = useMemo(() => { @@ -52,12 +54,12 @@ export const CameraWidgetEditorView: FC = props => if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1; return selectedEffects.findIndex(effect => (effect.effect.name === name)); - }, [ selectedEffects ]) + }, [ selectedEffects ]); const getCurrentEffectIndex = useMemo(() => { - return getSelectedEffectIndex(selectedEffectName) - }, [ selectedEffectName, getSelectedEffectIndex ]) + return getSelectedEffectIndex(selectedEffectName); + }, [ selectedEffectName, getSelectedEffectIndex ]); const getCurrentEffect = useMemo(() => { @@ -83,9 +85,66 @@ export const CameraWidgetEditorView: FC = props => }); }, [ getCurrentEffectIndex, setSelectedEffects ]); - const getCurrentPictureUrl = useMemo(() => + const applyEffectsWithFallback = async (texture: RenderTexture, effects: IRoomCameraWidgetSelectedEffect[], isZoomed: boolean): Promise => { - return GetRoomCameraWidgetManager().applyEffects(picture.texture, selectedEffects, isZoomed).src; + const result = GetRoomCameraWidgetManager().applyEffects(texture, effects, isZoomed); + let src = result?.src; + + if (!src || typeof src !== 'string' || !src.startsWith('data:image/')) { + + let sprite = new NitroSprite(texture); + let appliedEffects = false; + + for (const selectedEffect of effects) { + const effect = selectedEffect.effect; + const alpha = selectedEffect.alpha; + + if (effect.colorMatrix) { + const colorMatrixFilter = new ColorMatrixFilter(); + colorMatrixFilter.matrix = effect.colorMatrix; + colorMatrixFilter.alpha = alpha; + sprite.filters = (sprite.filters || []).concat(colorMatrixFilter); + appliedEffects = true; + } else if (effect.texture) { + const overlaySprite = new NitroSprite(effect.texture); + overlaySprite.alpha = alpha; + overlaySprite.width = texture.width; + overlaySprite.height = texture.height; + + const container = new NitroSprite(); + container.addChild(sprite, overlaySprite); + + sprite = container; + appliedEffects = true; + } + } + + if (appliedEffects) { + src = await TextureUtils.generateImageUrl(sprite); + } else { + src = await TextureUtils.generateImageUrl(texture); + } + + if (!src || typeof src !== 'string' || !src.startsWith('data:image/')) { + return null; + } + } + + return src; + }; + + useEffect(() => + { + if (!picture || !picture.texture) { + setCurrentPictureUrl(null); + return; + } + + applyEffectsWithFallback(picture.texture, selectedEffects, isZoomed) + .then(url => setCurrentPictureUrl(url)) + .catch(error => { + setCurrentPictureUrl(null); + }); }, [ picture, selectedEffects, isZoomed ]); const processAction = useCallback((type: string, effectName: string = null) => @@ -99,7 +158,9 @@ export const CameraWidgetEditorView: FC = props => onCancel(); return; case 'checkout': - onCheckout(getCurrentPictureUrl); + if (currentPictureUrl) { + onCheckout(currentPictureUrl); + } return; case 'change_tab': setCurrentTab(String(effectName)); @@ -145,7 +206,7 @@ export const CameraWidgetEditorView: FC = props => case 'download': { const image = new Image(); - image.src = getCurrentPictureUrl + image.src = currentPictureUrl || ''; const newWindow = window.open(''); newWindow.document.write(image.outerHTML); @@ -155,18 +216,32 @@ export const CameraWidgetEditorView: FC = props => setIsZoomed(!isZoomed); return; } - }, [ isZoomed, availableEffects, selectedEffectName, getCurrentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]); + }, [ isZoomed, availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]); useEffect(() => { - const thumbnails: CameraPictureThumbnail[] = []; - - for(const effect of availableEffects) - { - thumbnails.push(new CameraPictureThumbnail(effect.name, GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false).src)); + if (!picture || !picture.texture) { + setEffectsThumbnails([]); + return; } - setEffectsThumbnails(thumbnails); + const thumbnails: CameraPictureThumbnail[] = []; + + const generateThumbnails = async () => + { + for(const effect of availableEffects) + { + const thumbnailSrc = await applyEffectsWithFallback(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false); + if (thumbnailSrc) { + thumbnails.push(new CameraPictureThumbnail(effect.name, thumbnailSrc)); + } + } + setEffectsThumbnails(thumbnails); + }; + + generateThumbnails().catch(error => { + setEffectsThumbnails([]); + }); }, [ picture, availableEffects ]); return ( @@ -185,7 +260,11 @@ export const CameraWidgetEditorView: FC = props => - + { currentPictureUrl ? ( + + ) : ( + { LocalizeText('camera.loading.error') } + ) } { selectedEffectName && { LocalizeText('camera.effect.name.' + selectedEffectName) } @@ -195,7 +274,11 @@ export const CameraWidgetEditorView: FC = props => step={ 0.01 } value={ getCurrentEffect.alpha } onChange={ event => setSelectedEffectAlpha(event) } - renderThumb={ (props, state) =>
{ state.valueNow }
} /> + renderThumb={ (props, state) => { + const { key, ...restProps } = props; + return
{ state.valueNow }
; + } } + />
}
@@ -203,7 +286,7 @@ export const CameraWidgetEditorView: FC = props => - - @@ -225,4 +308,4 @@ export const CameraWidgetEditorView: FC = props => ); -} +} \ No newline at end of file diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index 507bb6c..4fe767d 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -53,7 +53,7 @@ export const CatalogGridOfferView: FC = props => return ( { (offer.product.productType === ProductTypeEnum.ROBOT) && - } + } ); } diff --git a/src/components/inventory/views/bot/InventoryBotItemView.tsx b/src/components/inventory/views/bot/InventoryBotItemView.tsx index 9bc89a2..13df425 100644 --- a/src/components/inventory/views/bot/InventoryBotItemView.tsx +++ b/src/components/inventory/views/bot/InventoryBotItemView.tsx @@ -1,58 +1,43 @@ import { MouseEventType } from "@nitrots/nitro-renderer"; import { FC, MouseEvent, PropsWithChildren, useState } from "react"; -import { - attemptBotPlacement, - IBotItem, - UnseenItemCategory, -} from "../../../../api"; +import { attemptBotPlacement, IBotItem, UnseenItemCategory, } from "../../../../api"; import { LayoutAvatarImageView, LayoutGridItem } from "../../../../common"; import { useInventoryBots, useInventoryUnseenTracker } from "../../../../hooks"; -export const InventoryBotItemView: FC< - PropsWithChildren<{ botItem: IBotItem }> -> = (props) => { - const { botItem = null, children = null, ...rest } = props; - const [isMouseDown, setMouseDown] = useState(false); - const { selectedBot = null, setSelectedBot = null } = useInventoryBots(); - const { isUnseen = null } = useInventoryUnseenTracker(); - const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id); +export const InventoryBotItemView: FC> = props => +{ + const { botItem = null, children = null, ...rest } = props; + const [ isMouseDown, setMouseDown ] = useState(false); + const { selectedBot = null, setSelectedBot = null } = useInventoryBots(); + const { isUnseen = null } = useInventoryUnseenTracker(); + const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id); - const onMouseEvent = (event: MouseEvent) => { - switch (event.type) { - case MouseEventType.MOUSE_DOWN: - setSelectedBot(botItem); - setMouseDown(true); - return; - case MouseEventType.MOUSE_UP: - setMouseDown(false); - return; - case MouseEventType.ROLL_OUT: - if (!isMouseDown || selectedBot !== botItem) return; + const onMouseEvent = (event: MouseEvent) => + { + switch(event.type) + { + case MouseEventType.MOUSE_DOWN: + setSelectedBot(botItem); + setMouseDown(true); + return; + case MouseEventType.MOUSE_UP: + setMouseDown(false); + return; + case MouseEventType.ROLL_OUT: + if(!isMouseDown || (selectedBot !== botItem)) return; - attemptBotPlacement(botItem); - return; - case "dblclick": - attemptBotPlacement(botItem); - return; - } - }; + attemptBotPlacement(botItem); + return; + case 'dblclick': + attemptBotPlacement(botItem); + return; + } + }; - return ( - - - {children} - - ); -}; + return ( + + + { children } + + ); +}; \ No newline at end of file diff --git a/src/components/notification-center/NotificationCenterView.scss b/src/components/notification-center/NotificationCenterView.scss index 85c2d1b..a65a82f 100644 --- a/src/components/notification-center/NotificationCenterView.scss +++ b/src/components/notification-center/NotificationCenterView.scss @@ -77,8 +77,7 @@ color: #fff; width: 400px; min-height: 400px; - max-height: 500px; - overflow-y: hidden; + max-height: 400px; border: 2px solid #ffd700; border-radius: 8px; box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); @@ -117,7 +116,7 @@ padding: 20px; position: relative; height: calc(100% - 34px); - overflow-y: hidden; + overflow-y: auto; /* Changed to auto to allow scrolling */ display: flex; justify-content: center; align-items: flex-start; @@ -125,12 +124,9 @@ } .grid { - animation: scroll-credits 20s linear infinite; + animation: scroll-credits 40s linear infinite; text-align: center; - position: absolute; width: 100%; - top: 0; - left: 0; will-change: transform; } @@ -169,8 +165,8 @@ background-repeat: no-repeat; filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7)); } - - .nitro-logo-default { + + .nitro-logo-default { width: 300px; height: 150px; background-image: url("@/assets/images/notifications/nitro.png"); @@ -178,16 +174,16 @@ margin: 0 auto 20px; filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7)); } - - .credits-divider { + + .credits-divider { width: 80%; height: 1px; background: #000; margin: 10px auto; opacity: 0.5; } - - .spacer { + + .spacer { height: 20px; width: 100%; } @@ -196,18 +192,14 @@ @keyframes scroll-credits { 0% { transform: translateY(100%); - opacity: 0; } - 10% { - transform: translateY(80%); - opacity: 1; + 5% { + transform: translateY(50%); } - 90% { - transform: translateY(-80%); - opacity: 1; + 95% { + transform: translateY(-110%); } 100% { - transform: translateY(-100%); - opacity: 0; + transform: translateY(-150%); } } \ No newline at end of file diff --git a/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx b/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx index 099a77b..41b1f7e 100644 --- a/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx +++ b/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx @@ -20,7 +20,7 @@ export const NitroSystemAlertView: FC = props Nitro React Nitro was created by billsonnn
- Nitro Versions + Nitro Versions Nitro: {GetUIVersion()}
@@ -57,29 +57,44 @@ export const NitroSystemAlertView: FC = props - DuckieTM (Re-Design) - Jonas (Contributing) - Ohlucas (Sunset resources) - v1.5.0 + v2.0.0
- - +
- - - - Special Thanks - The whole Discord community !! - - Billsonnn for creating Nitro. - - Remco for testing. - - Object from Atom. - - Habbo for providing the assets - - -
+ + + Special Thanks + The whole Discord community !! + - Billsonnn for creating Nitro. + - Remco for testing. + - Object from Atom. + - Habbo for providing the assets + + +
+ +
+
+ + + License + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + + +
); diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 33ea96f..66b9875 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -38,23 +38,24 @@ export const InfoStandWidgetFurniView: FC = props const [ songId, setSongId ] = useState(-1); const [ songName, setSongName ] = useState(''); const [ songCreator, setSongCreator ] = useState(''); - const [itemLocation, setItemLocation] = useState<{ x: number; y: number; z: number; }>({ x: -1, y: -1, z: -1 }); + const [ itemLocation, setItemLocation ] = useState<{ x: number; y: number; z: number; }>({ x: -1, y: -1, z: -1 }); + const [ furniImage, setFurniImage ] = useState(null); useSoundEvent(NowPlayingEvent.NPE_SONG_CHANGED, event => { setSongId(event.id); }, (isJukeBox || isSongDisk)); - useSoundEvent(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event => + useSoundEvent(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event => { - if(event.id !== songId) return; + if (event.id !== songId) return; const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(event.id); - if(!songInfo) return; + if (!songInfo) return; - setSongName(songInfo.name); - setSongCreator(songInfo.creator); + setSongName(songInfo.name || ''); + setSongCreator(songInfo.creator || ''); }, (isJukeBox || isSongDisk)); useEffect(() => @@ -75,95 +76,105 @@ export const InfoStandWidgetFurniView: FC = props let furniIsJukebox = false; let furniIsSongDisk = false; let furniSongId = -1; - - const roomObject = GetRoomEngine().getRoomObject( roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR ); - const location = roomObject.getLocation(); - if (location) { - setItemLocation({ x: location.x, y: location.y, z: location.z, }); - } + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); + const location = roomObject.getLocation(); + if (location) { + setItemLocation({ x: location.x, y: location.y, z: location.z }); + } const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST); - if(isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController) + if (isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController) { canMove = true; canRotate = !avatarInfo.isWallItem; - if(avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true; + if (avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true; } - if(avatarInfo.isAnyRoomController) + if (avatarInfo.isAnyRoomController) { canSeeFurniId = true; } - if((((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY) || ((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT) && isValidController)) canUse = true; - - if(avatarInfo.extraParam) - { - if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI) - { - const stuffData = (avatarInfo.stuffData as CrackableDataType); - + try { + if ( + avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY || + (avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER && isValidController) || + (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX && isValidController) || + (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT && isValidController) + ) { canUse = true; - isCrackable = true; - crackableHits = stuffData.hits; - crackableTarget = stuffData.target; } + } catch (error) { + console.warn('Error checking usage policy:', error); + } - else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) - { - const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist(); - - if(playlist) + if (avatarInfo.extraParam) + { + try { + if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI) { - furniSongId = playlist.nowPlayingSongId; + const stuffData = (avatarInfo.stuffData as CrackableDataType); + + canUse = true; + isCrackable = true; + crackableHits = stuffData.hits; + crackableTarget = stuffData.target; + } + else if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) + { + const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist(); + if (playlist) + { + furniSongId = playlist.nowPlayingSongId; + } + furniIsJukebox = true; + } + else if (avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0) + { + furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length)) || -1; + furniIsSongDisk = true; } - furniIsJukebox = true; - } - - else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0) - { - furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length)); - - furniIsSongDisk = true; - } - - if(godMode) - { - const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length); - - if(extraParam) + if (godMode) { - const parts = extraParam.split('\t'); + const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length); - for(const part of parts) + if (extraParam) { - const value = part.split('='); + const parts = extraParam.split('\t'); - if(value && (value.length === 2)) + for (const part of parts) { - furniKeyss.push(value[0]); - furniValuess.push(value[1]); + const value = part.split('='); + + if (value && (value.length === 2)) + { + furniKeyss.push(value[0]); + furniValuess.push(value[1]); + } } } } + } catch (error) { + console.warn('Error processing extraParam:', error); } } - if(godMode) + if (godMode) { const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, (avatarInfo.isWallItem) ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); - if(roomObject) + if (roomObject) { const customVariables = roomObject.model.getValue(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES); const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA); - if(customVariables && customVariables.length) + if (customVariables && customVariables.length) { - for(const customVariable of customVariables) + for (const customVariable of customVariables) { customKeyss.push(customVariable); customValuess.push((furnitureData[customVariable]) || ''); @@ -172,11 +183,10 @@ export const InfoStandWidgetFurniView: FC = props } } - if(avatarInfo.isOwner || avatarInfo.isAnyRoomController) pickupMode = PICKUP_MODE_FULL; + if (avatarInfo.isOwner || avatarInfo.isAnyRoomController) pickupMode = PICKUP_MODE_FULL; + else if (avatarInfo.isRoomOwner || (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN)) pickupMode = PICKUP_MODE_EJECT; - else if(avatarInfo.isRoomOwner || (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN)) pickupMode = PICKUP_MODE_EJECT; - - if(avatarInfo.isStickie) pickupMode = PICKUP_MODE_NONE; + if (avatarInfo.isStickie) pickupMode = PICKUP_MODE_NONE; setPickupMode(pickupMode); setCanMove(canMove); @@ -196,16 +206,26 @@ export const InfoStandWidgetFurniView: FC = props setIsSongDisk(furniIsSongDisk); setSongId(furniSongId); - if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false)); + if (avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false)); + + if (avatarInfo.image instanceof Promise) { + avatarInfo.image.then(image => { + setFurniImage(image); + }).catch(error => { + setFurniImage(null); + }); + } else { + setFurniImage(avatarInfo.image); + } }, [ roomSession, avatarInfo ]); useMessageEvent(GroupInformationEvent, event => { const parser = event.getParser(); - if(!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return; + if (!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return; - if(groupName) setGroupName(null); + if (groupName) setGroupName(null); setGroupName(parser.title); }); @@ -213,44 +233,36 @@ export const InfoStandWidgetFurniView: FC = props useEffect(() => { const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(songId); - - setSongName(songInfo?.name ?? ''); - setSongCreator(songInfo?.creator ?? ''); + setSongName(songInfo && songInfo.name ? songInfo.name : ''); + setSongCreator(songInfo && songInfo.creator ? songInfo.creator : ''); }, [ songId ]); const onFurniSettingChange = useCallback((index: number, value: string) => { const clone = Array.from(furniValues); - clone[index] = value; - setFurniValues(clone); }, [ furniValues ]); const onCustomVariableChange = useCallback((index: number, value: string) => { const clone = Array.from(customValues); - clone[index] = value; - setCustomValues(clone); }, [ customValues ]); const getFurniSettingsAsString = useCallback(() => { - if(furniKeys.length === 0 || furniValues.length === 0) return ''; + if (furniKeys.length === 0 || furniValues.length === 0) return ''; let data = ''; - let i = 0; - while(i < furniKeys.length) + while (i < furniKeys.length) { const key = furniKeys[i]; const value = furniValues[i]; - data = (data + (key + '=' + value + '\t')); - i++; } @@ -259,11 +271,11 @@ export const InfoStandWidgetFurniView: FC = props const processButtonAction = useCallback((action: string) => { - if(!action || (action === '')) return; + if (!action || (action === '')) return; let objectData: string = null; - switch(action) + switch (action) { case 'buy_one': CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`); @@ -275,7 +287,7 @@ export const InfoStandWidgetFurniView: FC = props GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE); break; case 'pickup': - if(pickupMode === PICKUP_MODE_FULL) + if (pickupMode === PICKUP_MODE_FULL) { GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP); } @@ -291,12 +303,11 @@ export const InfoStandWidgetFurniView: FC = props const mapData = new Map(); const dataParts = getFurniSettingsAsString().split('\t'); - if(dataParts) + if (dataParts) { - for(const part of dataParts) + for (const part of dataParts) { const [ key, value ] = part.split('=', 2); - mapData.set(key, value); } } @@ -307,12 +318,12 @@ export const InfoStandWidgetFurniView: FC = props case 'save_custom_variables': { const map = new Map(); - for(let i = 0; i < customKeys.length; i++) + for (let i = 0; i < customKeys.length; i++) { const key = customKeys[i]; const value = customValues[i]; - if((key && key.length) && (value && value.length)) map.set(key, value); + if ((key && key.length) && (value && value.length)) map.set(key, value); } SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map)); @@ -325,12 +336,12 @@ export const InfoStandWidgetFurniView: FC = props { const stringDataType = (avatarInfo.stuffData as StringDataType); - if(!stringDataType || !(stringDataType instanceof StringDataType)) return null; + if (!stringDataType || !(stringDataType instanceof StringDataType)) return null; return stringDataType.getValue(2); }, [ avatarInfo ]); - if(!avatarInfo) return null; + if (!avatarInfo) return null; return ( @@ -339,7 +350,7 @@ export const InfoStandWidgetFurniView: FC = props { !(isSongDisk) && { avatarInfo.name } } - { (songName.length > 0) && { songName } } + { songName && songName.length > 0 && { songName } }
@@ -354,8 +365,12 @@ export const InfoStandWidgetFurniView: FC = props
} - { avatarInfo.image && avatarInfo.image.src.length && - } + { furniImage && furniImage.src && typeof furniImage.src === 'string' && furniImage.src.length > 0 ? + <> + + : + console.log('Skipping furni image: Invalid or missing src', furniImage) + }
@@ -384,14 +399,14 @@ export const InfoStandWidgetFurniView: FC = props { LocalizeText('infostand.jukebox.text.not.playing') } } - { !!songName.length && + { songName && songName.length > 0 && { songName } } - { !!songCreator.length && + { songCreator && songCreator.length > 0 && @@ -413,15 +428,15 @@ export const InfoStandWidgetFurniView: FC = props { groupName } } - <> -
- - X = {itemLocation.x} and Y = {itemLocation.y}
- BuildHeight = {itemLocation.z < 0.01 ? 0 : itemLocation.z}
- { canSeeFurniId && Room Furnishing ID: { avatarInfo.id } } -
- - {itemLocation.x > -1} + <> +
+ + X = {itemLocation.x} and Y = {itemLocation.y}
+ BuildHeight = {itemLocation.z < 0.01 ? 0 : itemLocation.z}
+ { canSeeFurniId && Room Furnishing ID: { avatarInfo.id } } +
+ + {itemLocation.x > -1} { godMode && <>
diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 393e684..6f2d7ab 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -157,7 +157,7 @@ export const InfoStandWidgetUserView: FC = props = GetUserProfile(avatarInfo.webID) }> - + { avatarInfo.type === AvatarInfoUser.OWN_USER && diff --git a/src/components/toolbar/ToolbarMeView.tsx b/src/components/toolbar/ToolbarMeView.tsx index 7dcafda..64eabbc 100644 --- a/src/components/toolbar/ToolbarMeView.tsx +++ b/src/components/toolbar/ToolbarMeView.tsx @@ -1,6 +1,6 @@ import { MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react'; -import { CreateLinkEvent, DispatchUiEvent, GetConfiguration, GetRoomEngine, GetRoomSession, GetUserProfile } from '../../api'; +import { CreateLinkEvent, DispatchUiEvent, GetConfiguration, GetRoomEngine, GetRoomSession, GetSessionDataManager, GetUserProfile } from '../../api'; import { Base, Flex, LayoutItemCountView } from '../../common'; import { GuideToolEvent } from '../../events'; diff --git a/submodules/renderer/package.json b/submodules/renderer/package.json index 256682f..0374f03 100644 --- a/submodules/renderer/package.json +++ b/submodules/renderer/package.json @@ -22,38 +22,39 @@ }, "main": "./index", "dependencies": { - "@pixi/app": "~6.5.10", - "@pixi/basis": "~6.5.10", - "@pixi/canvas-display": "~6.5.10", - "@pixi/canvas-extract": "~6.5.10", - "@pixi/canvas-renderer": "~6.5.10", - "@pixi/constants": "~6.5.10", - "@pixi/core": "~6.5.10", - "@pixi/display": "~6.5.10", - "@pixi/events": "~6.5.10", - "@pixi/extensions": "~6.5.10", - "@pixi/extract": "~6.5.10", - "@pixi/filter-alpha": "~6.5.10", - "@pixi/filter-color-matrix": "~6.5.10", - "@pixi/graphics": "~6.5.10", - "@pixi/graphics-extras": "~6.5.10", + "@pixi/app": "~7.4.3", + "@pixi/assets": "~7.4.3", + "@pixi/basis": "~7.4.3", + "@pixi/canvas-display": "~7.4.3", + "@pixi/canvas-extract": "~7.4.3", + "@pixi/canvas-renderer": "~7.4.3", + "@pixi/constants": "~7.4.3", + "@pixi/core": "~7.4.3", + "@pixi/display": "~7.4.3", + "@pixi/events": "~7.4.3", + "@pixi/extensions": "~7.4.3", + "@pixi/extract": "~7.4.3", + "@pixi/filter-alpha": "~7.4.3", + "@pixi/filter-color-matrix": "~7.4.3", + "@pixi/graphics": "~7.4.3", + "@pixi/graphics-extras": "~7.4.3", "@pixi/interaction": "~6.5.10", "@pixi/loaders": "~6.5.10", - "@pixi/math": "~6.5.10", - "@pixi/math-extras": "~6.5.10", - "@pixi/mixin-cache-as-bitmap": "~6.5.10", - "@pixi/mixin-get-child-by-name": "~6.5.10", - "@pixi/mixin-get-global-position": "~6.5.10", + "@pixi/math": "~7.4.3", + "@pixi/math-extras": "~7.4.3", + "@pixi/mixin-cache-as-bitmap": "~7.4.3", + "@pixi/mixin-get-child-by-name": "~7.4.3", + "@pixi/mixin-get-global-position": "~7.4.3", "@pixi/polyfill": "~6.5.10", - "@pixi/runner": "~6.5.10", - "@pixi/settings": "~6.5.10", - "@pixi/sprite": "~6.5.10", - "@pixi/sprite-tiling": "~6.5.10", - "@pixi/spritesheet": "~6.5.10", - "@pixi/text": "~6.5.10", - "@pixi/ticker": "~6.5.10", - "@pixi/tilemap": "^3.2.2", - "@pixi/utils": "~6.5.10", + "@pixi/runner": "~7.4.3", + "@pixi/settings": "~7.4.3", + "@pixi/sprite": "~7.4.3", + "@pixi/sprite-tiling": "~7.4.3", + "@pixi/spritesheet": "~7.4.3", + "@pixi/text": "~7.4.3", + "@pixi/ticker": "~7.4.3", + "@pixi/tilemap": "^5.0.1", + "@pixi/utils": "~7.4.3", "clientjs": "^0.2.1", "gifuct-js": "^2.1.2", "howler": "^2.2.3", @@ -68,7 +69,7 @@ "@typescript-eslint/parser": "^5.30.7", "eslint": "^8.20.0", "tslib": "^2.3.1", - "typescript": "~4.4.4", + "typescript": "~5.4.5", "vite": "^4.0.2", "vite-plugin-minify": "^1.5.2" } diff --git a/submodules/renderer/src/api/nitro/avatar/IAvatarImage.ts b/submodules/renderer/src/api/nitro/avatar/IAvatarImage.ts index 559a7d3..e6b9064 100644 --- a/submodules/renderer/src/api/nitro/avatar/IAvatarImage.ts +++ b/submodules/renderer/src/api/nitro/avatar/IAvatarImage.ts @@ -17,7 +17,7 @@ export interface IAvatarImage extends IDisposable getLayerData(_arg_1: ISpriteDataContainer): IAnimationLayerData; getImage(setType: string, hightlight: boolean, scale?: number, cache?: boolean): RenderTexture; getImageAsSprite(setType: string, scale?: number): Sprite; - getCroppedImage(setType: string, scale?: number): HTMLImageElement; + getCroppedImage(setType: string, scale?: number): Promise; // Ensure async getAsset(_arg_1: string): IGraphicAsset; getDirection(): number; getFigure(): IAvatarFigureContainer; @@ -33,4 +33,4 @@ export interface IAvatarImage extends IDisposable animationHasResetOnToggle: boolean; resetAnimationFrameCounter(): void; mainAction: string; -} +} \ No newline at end of file diff --git a/submodules/renderer/src/core/NitroVersion.ts b/submodules/renderer/src/core/NitroVersion.ts index 72c3105..a418358 100644 --- a/submodules/renderer/src/core/NitroVersion.ts +++ b/submodules/renderer/src/core/NitroVersion.ts @@ -1,6 +1,6 @@ export class NitroVersion { - public static RENDERER_VERSION: string = '1.6.6'; + public static RENDERER_VERSION: string = '1.7.4'; public static UI_VERSION: string = ''; public static sayHello(): void diff --git a/submodules/renderer/src/nitro/Plugins.ts b/submodules/renderer/src/nitro/Plugins.ts index 6fe069f..5dc1fc7 100644 --- a/submodules/renderer/src/nitro/Plugins.ts +++ b/submodules/renderer/src/nitro/Plugins.ts @@ -1,6 +1,5 @@ import '@pixi/canvas-display'; -import { BatchRenderer, extensions } from '@pixi/core'; -import { Extract } from '@pixi/extract'; +import { extensions } from '@pixi/core'; import '@pixi/graphics-extras'; import { AppLoaderPlugin } from '@pixi/loaders'; import '@pixi/math-extras'; @@ -9,13 +8,8 @@ import '@pixi/mixin-get-child-by-name'; import '@pixi/mixin-get-global-position'; import '@pixi/polyfill'; import { TilingSpriteRenderer } from '@pixi/sprite-tiling'; -import { SpritesheetLoader } from '@pixi/spritesheet'; import { TickerPlugin } from '@pixi/ticker'; extensions.add( - BatchRenderer, - Extract, - TilingSpriteRenderer, - SpritesheetLoader, - AppLoaderPlugin, - TickerPlugin); + TilingSpriteRenderer +); \ No newline at end of file diff --git a/submodules/renderer/src/nitro/avatar/AvatarImage.ts b/submodules/renderer/src/nitro/avatar/AvatarImage.ts index ef6f7fa..24685c6 100644 --- a/submodules/renderer/src/nitro/avatar/AvatarImage.ts +++ b/submodules/renderer/src/nitro/avatar/AvatarImage.ts @@ -504,58 +504,60 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener return container; } - public getCroppedImage(setType: string, scale: number = 1): HTMLImageElement + public async getCroppedImage(setType: string, scale: number = 1): Promise { - if(!this._mainAction) return null; + if (!this._mainAction) { + console.warn('getCroppedImage: No main action'); + return null; + } - if(!this._actionsSorted) this.endActionAppends(); + if (!this._actionsSorted) this.endActionAppends(); const avatarCanvas = this._structure.getCanvas(this._scale, this._mainAction.definition.geometryType); - if(!avatarCanvas) return null; + if (!avatarCanvas) { + console.warn('getCroppedImage: No avatar canvas'); + return null; + } const setTypes = this.getBodyParts(setType, this._mainAction.definition.geometryType, this._mainDirection); const container = new NitroContainer(); - let partCount = (setTypes.length - 1); + let partCount = setTypes.length - 1; + let partsAdded = 0; - while(partCount >= 0) + while (partCount >= 0) { const set = setTypes[partCount]; const part = this._cache.getImageContainer(set, this._frameCounter); - if(part) + if (part) { const partCacheContainer = part.image; - if(!partCacheContainer) - { - container.destroy({ - children: true - }); - + if (!partCacheContainer) { + console.warn(`getCroppedImage: No image for part ${set}`); + container.destroy({ children: true }); return null; } const point = part.regPoint.clone(); - if(point) + if (point) { point.x += avatarCanvas.offset.x; point.y += avatarCanvas.offset.y; - point.x += avatarCanvas.regPoint.x; point.y += avatarCanvas.regPoint.y; const partContainer = new NitroContainer(); - partContainer.addChild(partCacheContainer); - if(partContainer) + if (partContainer) { partContainer.position.set(point.x, point.y); - container.addChild(partContainer); + partsAdded++; } } } @@ -565,9 +567,22 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener const texture = TextureUtils.generateTexture(container, new Rectangle(0, 0, avatarCanvas.width, avatarCanvas.height)); - const image = TextureUtils.generateImage(texture); + if (!texture) { + console.warn('getCroppedImage: Failed to generate texture'); + container.destroy({ children: true }); + return null; + } - if(!image) return null; + const image = await TextureUtils.generateImage(texture); + + container.destroy({ children: true }); + + if (!image || !image.src) { + console.warn('getCroppedImage: Invalid image generated', image); + const fallback = new Image(); + fallback.src = ''; + return fallback; + } return image; } @@ -1058,4 +1073,4 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener if(this._effectListener) this._effectListener.resetEffect(effect); } } -} +} \ No newline at end of file diff --git a/submodules/renderer/src/nitro/communication/messages/outgoing/camera/RenderRoomMessageComposer.ts b/submodules/renderer/src/nitro/communication/messages/outgoing/camera/RenderRoomMessageComposer.ts index 940b731..5e561e1 100644 --- a/submodules/renderer/src/nitro/communication/messages/outgoing/camera/RenderRoomMessageComposer.ts +++ b/submodules/renderer/src/nitro/communication/messages/outgoing/camera/RenderRoomMessageComposer.ts @@ -25,7 +25,11 @@ export class RenderRoomMessageComposer implements IMessageComposer c.charCodeAt(0)); @@ -35,9 +39,15 @@ export class RenderRoomMessageComposer implements IMessageComposer c.charCodeAt(0)); this._data.push(binaryData.byteLength, binaryData.buffer); } -} +} \ No newline at end of file diff --git a/submodules/renderer/src/nitro/room/object/logic/furniture/FurnitureChangeStateWhenStepOnLogic.ts b/submodules/renderer/src/nitro/room/object/logic/furniture/FurnitureChangeStateWhenStepOnLogic.ts index 14cec74..6341641 100644 --- a/submodules/renderer/src/nitro/room/object/logic/furniture/FurnitureChangeStateWhenStepOnLogic.ts +++ b/submodules/renderer/src/nitro/room/object/logic/furniture/FurnitureChangeStateWhenStepOnLogic.ts @@ -37,7 +37,7 @@ export class FurnitureChangeStateWhenStepOnLogic extends FurnitureLogic let sizeX = this.object.model.getValue(RoomObjectVariable.FURNITURE_SIZE_X); let sizeY = this.object.model.getValue(RoomObjectVariable.FURNITURE_SIZE_Y); - const direction = (((Math.floor(this.object.getDirection().x) + 45) % 360) / 90); + const direction = Math.floor(((this.object.getDirection().x + 45) % 360) / 90); if((direction === 1) || (direction === 3)) [sizeX, sizeY] = [sizeY, sizeX]; diff --git a/submodules/renderer/src/nitro/room/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts b/submodules/renderer/src/nitro/room/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts index 9bca1b5..ef17ab7 100644 --- a/submodules/renderer/src/nitro/room/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts +++ b/submodules/renderer/src/nitro/room/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts @@ -14,37 +14,51 @@ export class FurnitureDynamicThumbnailVisualization extends IsometricImageFurniV this._hasOutline = true; } - protected updateModel(scale: number): boolean { - if (this.object) { - const thumbnailUrl = this.getThumbnailURL(); + protected async updateModel(scale: number): Promise + { + if (this.object) + { + const thumbnailUrl = this.getThumbnailURL(); - if (this._cachedUrl !== thumbnailUrl) { - this._cachedUrl = thumbnailUrl; + if (this._cachedUrl !== thumbnailUrl) + { + this._cachedUrl = thumbnailUrl; - if (this._cachedUrl && this._cachedUrl !== '') { - const image = new Image(); + if (this._cachedUrl && this._cachedUrl !== '') + { + const image = new Image(); - image.src = thumbnailUrl; - image.crossOrigin = '*'; + image.src = thumbnailUrl; + image.crossOrigin = '*'; - image.onload = () => { - const texture = Texture.from(image); + await new Promise((resolve) => { + image.onload = () => { + const texture = Texture.from(image); - texture.baseTexture.scaleMode = SCALE_MODES.LINEAR; + texture.baseTexture.scaleMode = SCALE_MODES.LINEAR; - this.setThumbnailImages(texture); - }; - } else { - this.setThumbnailImages(null); + this.setThumbnailImages(texture); + resolve(); + }; + image.onerror = () => { + console.warn('FurnitureDynamicThumbnailVisualization: Failed to load thumbnail image', { thumbnailUrl }); + this.setThumbnailImages(null); + resolve(); + }; + }); + } + else + { + this.setThumbnailImages(null); + } } } - } - return super.updateModel(scale); -} + return await super.updateModel(scale); + } protected getThumbnailURL(): string { throw (new Error('This method must be overridden!')); } -} +} \ No newline at end of file diff --git a/submodules/renderer/src/nitro/room/object/visualization/furniture/IsometricImageFurniVisualization.ts b/submodules/renderer/src/nitro/room/object/visualization/furniture/IsometricImageFurniVisualization.ts index 78d11c9..2cbaa27 100644 --- a/submodules/renderer/src/nitro/room/object/visualization/furniture/IsometricImageFurniVisualization.ts +++ b/submodules/renderer/src/nitro/room/object/visualization/furniture/IsometricImageFurniVisualization.ts @@ -12,6 +12,8 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza private _thumbnailImageNormal: Texture; private _thumbnailDirection: number; private _thumbnailChanged: boolean; + private _uniqueId: string; + private _photoUrl: string; protected _hasOutline: boolean; constructor() @@ -22,7 +24,9 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza this._thumbnailImageNormal = null; this._thumbnailDirection = -1; this._thumbnailChanged = false; - this._hasOutline = false; + this._uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + this._photoUrl = null; + this._hasOutline = true; } public get hasThumbnailImage(): boolean @@ -30,58 +34,83 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza return !(this._thumbnailImageNormal == null); } - public setThumbnailImages(texture: Texture): void + public setThumbnailImages(k: Texture, url?: string): void { - this._thumbnailImageNormal = texture; + this._thumbnailImageNormal = k; + this._photoUrl = url || null; this._thumbnailChanged = true; } - protected updateModel(scale: number): boolean + public getPhotoUrl(): string { - const flag = super.updateModel(scale); + return this._photoUrl; + } - if(!this._thumbnailChanged && (this._thumbnailDirection === this.direction)) return flag; + protected async updateModel(scale: number): Promise + { + const flag = await super.updateModel(scale); - this.refreshThumbnail(); + if (!this._thumbnailChanged && (this._thumbnailDirection === this.direction)) { + return flag; + } + + await this.refreshThumbnail(); return true; } - private refreshThumbnail(): void + private async refreshThumbnail(): Promise { - if(this.asset == null) return; - - if(this._thumbnailImageNormal) - { - this.addThumbnailAsset(this._thumbnailImageNormal, 64); + if (this.asset == null) { + return; } - else - { - this.asset.disposeAsset(this.getThumbnailAssetName(64)); + + const thumbnailAssetName = this.getThumbnailAssetName(64); + + if (this._thumbnailImageNormal) { + await this.addThumbnailAsset(this._thumbnailImageNormal, 64); + } else { + const layerId = 2; + const sprite = this.getSprite(layerId); } this._thumbnailChanged = false; this._thumbnailDirection = this.direction; } - private addThumbnailAsset(texture: Texture, scale: number): void + private async addThumbnailAsset(k: Texture, scale: number): Promise { let layerId = 0; - while(layerId < this.totalSprites) + while (layerId < this.totalSprites) { - if(this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL) + const layerTag = this.getLayerTag(scale, this.direction, layerId); + + if (layerTag === IsometricImageFurniVisualization.THUMBNAIL) { const assetName = (this.cacheSpriteAssetName(scale, layerId, false) + this.getFrameNumber(scale, layerId)); const asset = this.getAsset(assetName, layerId); + const thumbnailAssetName = `${this.getThumbnailAssetName(scale)}-${this._uniqueId}`; + const transformedTexture = await this.generateTransformedThumbnail(k, asset || { width: 64, height: 64, offsetX: 0, offsetY: 0 }); - if(asset) - { - const _local_6 = this.generateTransformedThumbnail(texture, asset); - const _local_7 = this.getThumbnailAssetName(scale); + if (!transformedTexture) { + console.warn('IsometricImageFurniVisualization: Failed to generate transformed thumbnail for asset', { assetName }); + return; + } - this.asset.disposeAsset(_local_7); - this.asset.addAsset(_local_7, _local_6, true, asset.offsetX, asset.offsetY, false, false); + const baseOffsetX = asset?.offsetX || 0; + const baseOffsetY = asset?.offsetY || 0; + + const offsetX = baseOffsetX - (transformedTexture.width / 2); + const offsetY = baseOffsetY - (transformedTexture.height / 2); + + this.asset.addAsset(thumbnailAssetName, transformedTexture, true, offsetX, offsetY, false, false); + + const sprite = this.getSprite(layerId); + if (sprite) { + sprite.texture = transformedTexture; + } else { + console.warn('IsometricImageFurniVisualization: Sprite not found for layer', { layerId }); } return; @@ -91,71 +120,79 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza } } - protected generateTransformedThumbnail(texture: Texture, asset: IGraphicAsset): Texture + protected async generateTransformedThumbnail(texture: Texture, asset: IGraphicAsset): Promise> { - if(this._hasOutline) - { - const container = new NitroSprite(); - const background = new NitroSprite(NitroTexture.WHITE); + const sprite = new NitroSprite(texture); - background.tint = 0x000000; - background.width = (texture.width + 40); - background.height = (texture.height + 40); - - const sprite = new NitroSprite(texture); - const offsetX = ((background.width - sprite.width) / 2); - const offsetY = ((background.height - sprite.height) / 2); - - sprite.position.set(offsetX, offsetY); - - container.addChild(background, sprite); - - texture = TextureUtils.generateTexture(container); - } - - texture.orig.width = asset.width; - texture.orig.height = asset.height; + const photoContainer = new NitroSprite(); + sprite.position.set(0, 0); + photoContainer.addChild(sprite); + const scaleFactor = (asset?.width || 64) / texture.width; const matrix = new Matrix(); - switch(this.direction) - { + switch (this.direction) { case 2: - matrix.b = -(0.5); - matrix.d /= 1.6; - matrix.ty = ((0.5) * texture.width); + matrix.a = scaleFactor; + matrix.b = (-0.5 * scaleFactor); + matrix.c = 0; + matrix.d = scaleFactor; + matrix.tx = 0; + matrix.ty = (0.5 * scaleFactor * texture.width); break; case 0: case 4: - matrix.b = (0.5); - matrix.d /= 1.6; - matrix.tx = -0.5; + matrix.a = scaleFactor; + matrix.b = (0.5 * scaleFactor); + matrix.c = 0; + matrix.d = scaleFactor; + matrix.tx = 0; + matrix.ty = 0; break; + default: + matrix.a = scaleFactor; + matrix.b = 0; + matrix.c = 0; + matrix.d = scaleFactor; + matrix.tx = 0; + matrix.ty = 0; } - const sprite = new NitroSprite(texture); + photoContainer.transform.setFromMatrix(matrix); - sprite.transform.setFromMatrix(matrix); + const width = 64; + const height = 64; - return TextureUtils.generateTexture(sprite); + const container = new NitroSprite(); + + photoContainer.position.set(width / 2, height / 2); + container.addChild(photoContainer); + + const renderTexture = await TextureUtils.generateTexture(container, null, null, 1); + if (!renderTexture) { + console.warn('IsometricImageFurniVisualization: Failed to generate render texture for thumbnail'); + return texture; + } + + return renderTexture; } protected getSpriteAssetName(scale: number, layerId: number): string { - if(this._thumbnailImageNormal && (this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL)) return this.getThumbnailAssetName(scale); + if (this._thumbnailImageNormal && (this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL)) { + return `${this.getThumbnailAssetName(scale)}-${this._uniqueId}`; + } return super.getSpriteAssetName(scale, layerId); } protected getThumbnailAssetName(scale: number): string { - this._thumbnailAssetNameNormal = this.getFullThumbnailAssetName(this.object.id, 64); - - return this._thumbnailAssetNameNormal; + return this.cacheSpriteAssetName(scale, 2, false) + this.getFrameNumber(scale, 2); } protected getFullThumbnailAssetName(k: number, _arg_2: number): string { return [this._type, k, 'thumb', _arg_2].join('_'); } -} +} \ No newline at end of file diff --git a/submodules/renderer/src/pixi-proxy/RoomTextureUtils.ts b/submodules/renderer/src/pixi-proxy/RoomTextureUtils.ts index f1f0579..faa9f20 100644 --- a/submodules/renderer/src/pixi-proxy/RoomTextureUtils.ts +++ b/submodules/renderer/src/pixi-proxy/RoomTextureUtils.ts @@ -12,31 +12,42 @@ export class PlaneTextureCache public RENDER_TEXTURE_POOL: Map = new Map(); public RENDER_TEXTURE_CACHE: RenderTexture[] = []; + // Store an Extract instance + private _extract: Extract; + + constructor() + { + // Initialize Extract with the renderer + const renderer = this.getRenderer(); + if (renderer) { + this._extract = new Extract(renderer); + } + } + public clearCache(): void { this.RENDER_TEXTURE_POOL.forEach(renderTexture => renderTexture?.destroy(true)); - this.RENDER_TEXTURE_POOL.clear(); this.RENDER_TEXTURE_CACHE = []; } public clearRenderTexture(renderTexture: RenderTexture): RenderTexture { - if(!renderTexture) return null; + if (!renderTexture) return null; return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture); } private getTextureIdentifier(width: number, height: number, planeId: string): string { - return `${ planeId ?? PlaneTextureCache.DEFAULT_PLANE_ID }:${ width }:${ height }`; + return `${planeId ?? PlaneTextureCache.DEFAULT_PLANE_ID}:${width}:${height}`; } public createRenderTexture(width: number, height: number, planeId: string = null): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; - if(!planeId) + if (!planeId) { const renderTexture = RenderTexture.create({ width, @@ -52,7 +63,7 @@ export class PlaneTextureCache let renderTexture = this.RENDER_TEXTURE_POOL.get(planeId); - if(!renderTexture) + if (!renderTexture) { renderTexture = RenderTexture.create({ width, @@ -60,7 +71,6 @@ export class PlaneTextureCache }); this.RENDER_TEXTURE_CACHE.push(renderTexture); - this.RENDER_TEXTURE_POOL.set(planeId, renderTexture); } @@ -69,7 +79,7 @@ export class PlaneTextureCache public createAndFillRenderTexture(width: number, height: number, planeId = null, color: number = 16777215): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; const renderTexture = this.createRenderTexture(width, height, planeId); @@ -78,7 +88,7 @@ export class PlaneTextureCache public createAndWriteRenderTexture(width: number, height: number, displayObject: DisplayObject, planeId: string = null, transform: Matrix = null): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; const renderTexture = this.createRenderTexture(width, height, planeId); @@ -87,12 +97,11 @@ export class PlaneTextureCache public clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture { - if(!renderTexture) return null; + if (!renderTexture) return null; const sprite = new Sprite(Texture.WHITE); sprite.tint = color; - sprite.width = renderTexture.width; sprite.height = renderTexture.height; @@ -101,7 +110,7 @@ export class PlaneTextureCache public writeToRenderTexture(displayObject: DisplayObject, renderTexture: RenderTexture, clear: boolean = true, transform: Matrix = null): RenderTexture { - if(!displayObject || !renderTexture) return null; + if (!displayObject || !renderTexture) return null; this.getRenderer().render(displayObject, { renderTexture, @@ -114,7 +123,7 @@ export class PlaneTextureCache public getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array { - return this.getExtractor().pixels(displayObject); + return this.getExtractor().pixels(displayObject, frame); } public getRenderer(): Renderer | AbstractRenderer @@ -124,6 +133,10 @@ export class PlaneTextureCache public getExtractor(): Extract { - return (this.getRenderer().plugins.extract as Extract); + // Return the stored Extract instance + if (!this._extract) { + throw new Error('Extract plugin not initialized. Ensure renderer is available.'); + } + return this._extract; } -} +} \ No newline at end of file diff --git a/submodules/renderer/src/pixi-proxy/TextureUtils.ts b/submodules/renderer/src/pixi-proxy/TextureUtils.ts index de4ab8f..d9d8465 100644 --- a/submodules/renderer/src/pixi-proxy/TextureUtils.ts +++ b/submodules/renderer/src/pixi-proxy/TextureUtils.ts @@ -8,11 +8,57 @@ import { PixiApplicationProxy } from './PixiApplicationProxy'; export class TextureUtils { + private static _extract: Extract | null = null; + + public static initialize(renderer: Renderer | AbstractRenderer): void + { + if (!this._extract && renderer) { + this._extract = new Extract(renderer); + console.log('TextureUtils: Initialized Extract plugin', { renderer }); + } + } + + public static async generateImage(target: DisplayObject | RenderTexture): Promise + { + if (!target) { + return null; + } + + const extractor = this.getExtractor(); + if (!extractor) { + return null; + } + + try { + const image = await extractor.image(target); + + if (!image || !image.src || typeof image.src !== 'string' || !image.src.startsWith('data:image/')) { + const canvas = extractor.canvas(target); + if (canvas) { + const dataUrl = canvas.toDataURL('image/png'); + if (dataUrl && dataUrl.startsWith('data:image/')) { + const fallbackImage = new Image(); + fallbackImage.src = dataUrl; + return fallbackImage; + } + } + const fallback = new Image(); + fallback.src = ''; + return fallback; + } + return image; + } catch (error) { + const fallback = new Image(); + fallback.src = ''; + return fallback; + } + } + public static generateTexture(displayObject: DisplayObject, region: Rectangle = null, scaleMode: number = null, resolution: number = 1): RenderTexture { - if(!displayObject) return null; + if (!displayObject) return null; - if(scaleMode === null) scaleMode = settings.SCALE_MODE; + if (scaleMode === null) scaleMode = settings.SCALE_MODE; return this.getRenderer().generateTexture(displayObject, { scaleMode, @@ -23,42 +69,70 @@ export class TextureUtils public static generateTextureFromImage(image: HTMLImageElement): Texture { - if(!image) return null; + if (!image) return null; return Texture.from(image); } - public static generateImage(target: DisplayObject | RenderTexture): HTMLImageElement + public static async generateImageUrl(target: DisplayObject | RenderTexture): Promise { - if(!target) return null; + if (!target) { + return null; + } - return this.getExtractor().image(target); - } + const extractor = this.getExtractor(); + if (!extractor) { + return null; + } - public static generateImageUrl(target: DisplayObject | RenderTexture): string - { - if(!target) return null; + let base64: string | Promise = extractor.base64(target); - return this.getExtractor().base64(target); + if (base64 && typeof base64 === 'object' && 'then' in base64) { + try { + base64 = await base64; + } catch (error) { + base64 = null; + } + } + + if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) { + const canvas = extractor.canvas(target); + if (canvas) { + const dataUrl = canvas.toDataURL('image/png'); + if (dataUrl && typeof dataUrl === 'string' && dataUrl.startsWith('data:image/')) { + return dataUrl; + } + } + return null; + } + + return base64; } public static generateCanvas(target: DisplayObject | RenderTexture): HTMLCanvasElement { - if(!target) return null; + if (!target) { + return null; + } - return this.getExtractor().canvas(target); + const extractor = this.getExtractor(); + if (!extractor) { + return null; + } + + return extractor.canvas(target); } public static clearRenderTexture(renderTexture: RenderTexture): RenderTexture { - if(!renderTexture) return null; + if (!renderTexture) return null; return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture); } public static createRenderTexture(width: number, height: number): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; return RenderTexture.create({ width, @@ -68,7 +142,7 @@ export class TextureUtils public static createAndFillRenderTexture(width: number, height: number, color: number = 16777215): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; const renderTexture = this.createRenderTexture(width, height); @@ -77,7 +151,7 @@ export class TextureUtils public static createAndWriteRenderTexture(width: number, height: number, displayObject: DisplayObject, transform: Matrix = null): RenderTexture { - if((width < 0) || (height < 0)) return null; + if (width < 0 || height < 0) return null; const renderTexture = this.createRenderTexture(width, height); @@ -86,12 +160,11 @@ export class TextureUtils public static clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture { - if(!renderTexture) return null; + if (!renderTexture) return null; const sprite = new Sprite(Texture.WHITE); sprite.tint = color; - sprite.width = renderTexture.width; sprite.height = renderTexture.height; @@ -100,7 +173,7 @@ export class TextureUtils public static writeToRenderTexture(displayObject: DisplayObject, renderTexture: RenderTexture, clear: boolean = true, transform: Matrix = null): RenderTexture { - if(!displayObject || !renderTexture) return null; + if (!displayObject || !renderTexture) return null; this.getRenderer().render(displayObject, { renderTexture, @@ -113,16 +186,33 @@ export class TextureUtils public static getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array { - return this.getExtractor().pixels(displayObject); + const extractor = this.getExtractor(); + if (!extractor) { + return null; + } + + return extractor.pixels(displayObject, frame); } public static getRenderer(): Renderer | AbstractRenderer { - return PixiApplicationProxy.instance.renderer; + const renderer = PixiApplicationProxy.instance.renderer; + if (!renderer) { + console.warn('getRenderer: Renderer not available'); + } + return renderer; } public static getExtractor(): Extract { - return (this.getRenderer().plugins.extract as Extract); + if (!this._extract) { + const renderer = this.getRenderer(); + if (renderer) { + this._extract = new Extract(renderer); + } else { + throw new Error('Cannot initialize Extract: Renderer not available'); + } + } + return this._extract; } -} +} \ No newline at end of file