🆙 First Stage !

This commit is contained in:
duckietm 2025-05-01 14:13:06 +02:00
parent ba63e68b92
commit a59c093e04
31 changed files with 1335 additions and 595 deletions

View File

@ -21,7 +21,7 @@ $toolbar-height: 55px;
$achievement-width: 375px; $achievement-width: 375px;
$achievement-height: 405px; $achievement-height: 405px;
$avatar-editor-width: 520px; $avatar-editor-width: 545px;
$avatar-editor-height: 553px; $avatar-editor-height: 553px;
$backgrounds-width: 534px; $backgrounds-width: 534px;

View File

@ -1 +1 @@
export const GetUIVersion = () => '2.1.1'; export const GetUIVersion = () => '2.2.5';

View File

@ -3,6 +3,9 @@ import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { GetAvatarRenderManager } from '../../api'; import { GetAvatarRenderManager } from '../../api';
import { Base, BaseProps } from '../Base'; import { Base, BaseProps } from '../Base';
// Cache for avatar image URLs
const AVATAR_IMAGE_CACHE: Map<string, { url: string }> = new Map();
export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement> export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
{ {
figure: string; figure: string;
@ -15,75 +18,166 @@ export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props => export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
{ {
const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(null); const [ avatarUrl, setAvatarUrl ] = useState<string | null>(null);
const [ randomValue, setRandomValue ] = useState(-1); const [ isReady, setIsReady ] = useState<boolean>(false);
const isDisposed = useRef(false); const isDisposed = useRef(false);
const retryCount = useRef(0);
const maxRetries = 3;
const elementRef = useRef<HTMLDivElement>(null);
const figureKey = useMemo(() => [ figure, gender, direction, headOnly ].join('-'), [ figure, gender, direction, headOnly ]);
const getClassNames = useMemo(() => const getClassNames = useMemo(() =>
{ {
const newClassNames: string[] = [ 'avatar-image' ]; const newClassNames: string[] = [ 'avatar-image' ];
if(headOnly) newClassNames.push('head-only');
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
}, [ classNames ]); }, [ classNames, headOnly ]);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
{ {
let newStyle: CSSProperties = {}; 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) if(scale !== 1)
{ {
newStyle.transform = `scale(${ scale })`; newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
} }
if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle; 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, { const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
resetFigure: figure => resetFigure: figure =>
{ {
if(isDisposed.current) return; if(isDisposed.current) return;
loadAvatarImage();
setRandomValue(Math.random());
}, },
dispose: () => dispose: () => {},
{},
disposed: false disposed: false
}, null); }, null);
if(!avatarImage) return; if(!avatarImage)
{
let setType = AvatarSetType.FULL; if(retryCount.current < maxRetries)
{
if(headOnly) setType = AvatarSetType.HEAD; retryCount.current += 1;
setTimeout(loadAvatarImage, 1000);
}
else
{
setAvatarUrl(null);
}
return;
}
try
{
const setType = headOnly ? AvatarSetType.HEAD : AvatarSetType.FULL;
avatarImage.setDirection(setType, direction); avatarImage.setDirection(setType, direction);
const image = avatarImage.getCroppedImage(setType); const image = await avatarImage.getCroppedImage(setType);
if(image) setAvatarUrl(image.src); if(isDisposed.current) return;
avatarImage.dispose(); if(image && image.src && typeof image.src === 'string' && image.src.startsWith('data:image/'))
}, [ figure, gender, direction, headOnly, randomValue ]); {
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(() => useEffect(() =>
{ {
isDisposed.current = false; isDisposed.current = false;
retryCount.current = 0;
setIsReady(true);
loadAvatarImage();
const handleVisibilityChange = () =>
{
if(document.visibilityState === 'visible')
{
setAvatarUrl(null);
loadAvatarImage();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => return () =>
{ {
isDisposed.current = true; isDisposed.current = true;
} document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []); };
}, [ figure, gender, direction, headOnly, figureKey ]);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />; return <Base innerRef={ elementRef } classNames={ getClassNames } style={ getStyle } { ...rest } />;
} };

View File

@ -23,9 +23,7 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
const newClassNames: string[] = [ 'badge-image' ]; const newClassNames: string[] = [ 'badge-image' ];
if(isGroup) newClassNames.push('group-badge'); if(isGroup) newClassNames.push('group-badge');
if(isGrayscale) newClassNames.push('grayscale'); if(isGrayscale) newClassNames.push('grayscale');
if(classNames.length) newClassNames.push(...classNames); if(classNames.length) newClassNames.push(...classNames);
return newClassNames; return newClassNames;
@ -37,16 +35,16 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
if(imageElement) if(imageElement)
{ {
newStyle.backgroundImage = `url(${ (isGroup) ? imageElement.src : GetConfiguration<string>('badge.asset.url').replace('%badgename%', badgeCode.toString())})`; const badgeUrl = isGroup ? imageElement.src : GetConfiguration<string>('badge.asset.url', '').replace('%badgename%', badgeCode.toString());
newStyle.backgroundImage = `url(${ badgeUrl })`;
newStyle.width = imageElement.width; newStyle.width = imageElement.width;
newStyle.height = imageElement.height; newStyle.height = imageElement.height;
if(scale !== 1) if(scale !== 1)
{ {
newStyle.transform = `scale(${ scale })`; newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
newStyle.width = (imageElement.width * scale); newStyle.width = (imageElement.width * scale);
newStyle.height = (imageElement.height * scale); newStyle.height = (imageElement.height * scale);
} }
@ -55,37 +53,81 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle; return newStyle;
}, [ imageElement, scale, style ]); }, [ badgeCode, imageElement, isGroup, scale, style ]);
useEffect(() => useEffect(() =>
{ {
if(!badgeCode || !badgeCode.length) return;
if(!badgeCode || !badgeCode.length)
{
console.warn('LayoutBadgeImageView: Invalid or empty badgeCode', badgeCode);
setImageElement(null);
return;
}
let didSetBadge = false; let didSetBadge = false;
const onBadgeImageReadyEvent = (event: BadgeImageReadyEvent) => const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) =>
{ {
if(event.badgeId !== badgeCode) return; if(event.badgeId !== badgeCode) return;
const element = TextureUtils.generateImage(new NitroSprite(event.image)); try
{
element.onload = () => setImageElement(element); const sprite = new NitroSprite(event.image);
const element = await TextureUtils.generateImage(sprite);
if(element && element.src && element.src.startsWith('data:image/'))
{
setImageElement(element);
didSetBadge = true; 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.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
} };
GetSessionDataManager().events.addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent); GetSessionDataManager().events.addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
const loadBadgeImage = async () =>
{
const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode); const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode);
if(texture && !didSetBadge) if(texture && !didSetBadge)
{ {
const element = TextureUtils.generateImage(new NitroSprite(texture)); try
{
const sprite = new NitroSprite(texture);
const element = await TextureUtils.generateImage(sprite);
element.onload = () => setImageElement(element); 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); return () => GetSessionDataManager().events.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
}, [ badgeCode, isGroup ]); }, [ badgeCode, isGroup ]);
@ -100,4 +142,4 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
{ children } { children }
</Base> </Base>
); );
} };

View File

@ -15,7 +15,7 @@ interface LayoutFurniImageViewProps extends BaseProps<HTMLDivElement>
export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props => export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = 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<HTMLImageElement>(null); const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getStyle = useMemo(() => const getStyle = useMemo(() =>
@ -24,15 +24,19 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
if(imageElement?.src?.length) if(imageElement?.src?.length)
{ {
console.log('LayoutFurniImageView: Applying image URL', imageElement.src);
newStyle.backgroundImage = `url('${ imageElement.src }')`; newStyle.backgroundImage = `url('${ imageElement.src }')`;
newStyle.width = imageElement.width; newStyle.width = imageElement.width;
newStyle.height = imageElement.height; newStyle.height = imageElement.height;
} }
else
{
console.log('LayoutFurniImageView: No imageElement, skipping style');
}
if(scale !== 1) if(scale !== 1)
{ {
newStyle.transform = `scale(${ scale })`; newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
} }
@ -43,40 +47,111 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
useEffect(() => 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; let imageResult: ImageResult = null;
const listener: IGetImageListener = { const listener: IGetImageListener = {
imageReady: (id, texture, image) => imageReady: async (id, texture, image) =>
{
console.log('LayoutFurniImageView: imageReady called', { id, texture, image });
try
{ {
if(!image && texture) if(!image && texture)
{ {
image = TextureUtils.generateImage(texture); image = await TextureUtils.generateImage(texture);
console.log('LayoutFurniImageView: Generated image from texture', image?.src);
} }
image.onload = () => setImageElement(image); 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
{
switch(productType.toLowerCase())
{ {
case ProductTypeEnum.FLOOR: case ProductTypeEnum.FLOOR:
console.log('LayoutFurniImageView: Fetching floor furniture image');
imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break; break;
case ProductTypeEnum.WALL: case ProductTypeEnum.WALL:
console.log('LayoutFurniImageView: Fetching wall furniture image');
imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData); imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break; break;
default:
console.warn('LayoutFurniImageView: Unknown productType', productType);
setImageElement(null);
return;
} }
if(imageResult) if(imageResult)
{ {
const image = imageResult.getImage(); const image = imageResult.getImage();
console.log('LayoutFurniImageView: Immediate imageResult', image?.src);
image.onload = () => setImageElement(image); 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');
}
}
catch(error)
{
console.warn('LayoutFurniImageView: Error fetching image', error);
setImageElement(null);
} }
}, [ productType, productClassId, direction, extraData ]); }, [ productType, productClassId, direction, extraData ]);
if(!imageElement) return null; if(!imageElement)
{
console.log('LayoutFurniImageView: Skipping render, no imageElement');
return null;
}
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />; return <Base classNames={ [ 'furni-image', ...classNames ] } style={ getStyle } { ...rest } />;
} };

View File

@ -14,41 +14,44 @@ interface LayoutPetImageViewProps extends BaseProps<HTMLDivElement>
headOnly?: boolean; headOnly?: boolean;
direction?: number; direction?: number;
scale?: number; scale?: number;
isIcon?: boolean;
} }
export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props => export const LayoutPetImageView: FC<LayoutPetImageViewProps> = 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<string>(null); const [ petUrl, setPetUrl ] = useState<string>(null);
const [ width, setWidth ] = useState(0); const [ width, setWidth ] = useState(0);
const [ height, setHeight ] = useState(0); const [ height, setHeight ] = useState(0);
const isDisposed = useRef(false); const isDisposed = useRef(false);
const retryCount = useRef(0);
const maxRetries = 3;
const prevFigure = useRef<string>('');
const getStyle = useMemo(() => const getStyle = useMemo(() =>
{ {
let newStyle: CSSProperties = {}; 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) if(scale !== 1)
{ {
newStyle.transform = `scale(${ scale })`; newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
} }
newStyle.width = width;
newStyle.height = height;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle; return newStyle;
}, [ petUrl, scale, style, width, height ]); }, [ petUrl, scale, style, width, height, isIcon ]);
useEffect(() => const fetchPetImage = () =>
{ {
let url = null;
let petTypeId = typeId; let petTypeId = typeId;
let petPaletteId = paletteId; let petPaletteId = paletteId;
let petColor1 = petColor; let petColor1 = petColor;
@ -56,66 +59,178 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
let petHeadOnly = headOnly; let petHeadOnly = headOnly;
if(figure && figure.length) if(figure && figure.length)
{
try
{ {
const petFigureData = new PetFigureData(figure); const petFigureData = new PetFigureData(figure);
petTypeId = petFigureData.typeId; petTypeId = petFigureData.typeId;
petPaletteId = petFigureData.paletteId; petPaletteId = petFigureData.paletteId;
petColor1 = petFigureData.color; petColor1 = petFigureData.color;
petCustomParts = petFigureData.customParts; petCustomParts = petFigureData.customParts;
} }
catch(error)
{
console.warn(`LayoutPetImageView: Error parsing PetFigureData (isIcon: ${isIcon})`, error, 'Figure:', figure);
setPetUrl(null);
return;
}
}
if(petTypeId === 16) petHeadOnly = false; if(petTypeId === 16) petHeadOnly = false;
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, { if(petTypeId < 0 || petPaletteId < 0)
imageReady: (id, texture, image) => {
console.warn(`LayoutPetImageView: Invalid petTypeId or petPaletteId (isIcon: ${isIcon})`, { petTypeId, petPaletteId, figure });
setPetUrl(null);
return;
}
try
{
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), isIcon ? 32 : 64, {
imageReady: async (id, texture, image) =>
{ {
if(isDisposed.current) return; if(isDisposed.current) return;
if(image) 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); setPetUrl(image.src);
setWidth(image.width); setWidth(image.width);
setHeight(image.height); setHeight(image.height);
} }
}
else if(texture) else if(texture)
{ {
setPetUrl(TextureUtils.generateImageUrl(texture)); const url = await TextureUtils.generateImageUrl(texture);
setWidth(texture.width); if(url && url.startsWith('data:image/'))
setHeight(texture.height); {
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) => 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); }, petHeadOnly, 0, petCustomParts, posture);
if(imageResult) if(imageResult)
{ {
const image = imageResult.getImage(); (async () =>
{
const image = await imageResult.getImage();
if(image) 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); setPetUrl(image.src);
setWidth(image.width); setWidth(image.width);
setHeight(image.height); setHeight(image.height);
} }
} }
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]); })();
}
else
{
if(retryCount.current < maxRetries)
{
retryCount.current += 1;
console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`);
setTimeout(fetchPetImage, 1000);
}
}
}
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(() => useEffect(() =>
{ {
isDisposed.current = false; 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 () => return () =>
{ {
isDisposed.current = true; isDisposed.current = true;
} document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []); };
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction, isIcon ]);
const url = `url('${ petUrl }')`; return <Base classNames={ [ 'pet-image', isIcon ? 'pet-icon' : '', ...classNames ] } style={ getStyle } { ...rest } />;
};
return <Base classNames={ [ 'pet-image' ] } style={ getStyle } { ...rest } />;
}

View File

@ -24,25 +24,47 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
useEffect(() => useEffect(() =>
{ {
if(!roomPreviewer) return; if(!roomPreviewer || height <= 0)
{
return;
}
const update = (time: number) => const update = async (time: number) =>
{ {
if(!roomPreviewer || !renderingCanvas || !elementRef.current) return; if(!roomPreviewer || !renderingCanvas || !elementRef.current) return;
try
{
roomPreviewer.updatePreviewRoomView(); roomPreviewer.updatePreviewRoomView();
if(!renderingCanvas.canvasUpdated) return; if(!renderingCanvas.canvasUpdated) return;
elementRef.current.style.backgroundImage = `url(${ TextureUtils.generateImageUrl(renderingCanvas.master) })`; 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(!renderingCanvas)
{ {
if(elementRef.current && roomPreviewer) if(elementRef.current && roomPreviewer)
{
try
{ {
const computed = document.defaultView.getComputedStyle(elementRef.current, null); const computed = document.defaultView.getComputedStyle(elementRef.current, null);
let backgroundColor = computed.backgroundColor; let backgroundColor = computed.backgroundColor;
backgroundColor = ColorConverter.rgbStringToHex(backgroundColor); backgroundColor = ColorConverter.rgbStringToHex(backgroundColor);
@ -56,12 +78,26 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
const canvas = roomPreviewer.getRenderingCanvas(); const canvas = roomPreviewer.getRenderingCanvas();
if(canvas)
{
setRenderingCanvas(canvas); setRenderingCanvas(canvas);
canvas.canvasUpdated = true; canvas.canvasUpdated = true;
update(-1); 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');
}
} }
GetTicker().add(update); GetTicker().add(update);
@ -72,9 +108,15 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
const width = elementRef.current.parentElement.offsetWidth; const width = elementRef.current.parentElement.offsetWidth;
try
{
roomPreviewer.modifyRoomCanvas(width, height); roomPreviewer.modifyRoomCanvas(width, height);
update(-1); update(-1);
}
catch(error)
{
console.warn('LayoutRoomPreviewerView: Error resizing canvas', error);
}
}); });
resizeObserver.observe(elementRef.current); resizeObserver.observe(elementRef.current);
@ -82,10 +124,8 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
return () => return () =>
{ {
resizeObserver.disconnect(); resizeObserver.disconnect();
GetTicker().remove(update); GetTicker().remove(update);
} }
}, [ renderingCanvas, roomPreviewer, elementRef, height ]); }, [ renderingCanvas, roomPreviewer, elementRef, height ]);
return ( return (
@ -94,4 +134,4 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
{ children } { children }
</div> </div>
); );
} };

View File

@ -334,7 +334,7 @@
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
bottom: 125px; bottom: -125px;
margin: 0 auto; margin: 0 auto;
z-index: 4; z-index: 4;
} }
@ -359,7 +359,7 @@
} }
.choose-clothing { .choose-clothing {
width: 320px; width: 360px;
} }
.color-picker-frame { .color-picker-frame {
@ -477,8 +477,8 @@
.avatar-parts { .avatar-parts {
border: none !important; border: none !important;
height: 42px; height: 50px;
width: 42px; width: 50px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
border-radius: 2rem !important; border-radius: 2rem !important;

View File

@ -1,6 +1,6 @@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState, useRef } from 'react';
import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api'; import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api';
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common'; import { LayoutGridItem, LayoutGridItemProps } from '../../../../common';
import { AvatarEditorIcon } from '../AvatarEditorIcon'; import { AvatarEditorIcon } from '../AvatarEditorIcon';
export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps
@ -12,21 +12,104 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
{ {
const { partItem = null, children = null, ...rest } = props; const { partItem = null, children = null, ...rest } = props;
const [ updateId, setUpdateId ] = useState(-1); const [ updateId, setUpdateId ] = useState(-1);
const [ imageUrl, setImageUrl ] = useState<string | null>(null);
const [ isValid, setIsValid ] = useState<boolean>(true);
const isDisposed = useRef(false);
const retryCount = useRef(0);
const maxRetries = 1;
const hcDisabled = GetConfiguration<boolean>('hc.disabled', false); const hcDisabled = GetConfiguration<boolean>('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(() => 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; 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 ]); }, [ partItem ]);
if(!isValid) return null;
return ( return (
<div className="avatar-container"> <div className="avatar-container">
<LayoutGridItem className={ `avatar-parts ${ partItem.isSelected ? 'part-selected' : '' }` } itemImage={ (partItem.isClear ? undefined : partItem.imageUrl) } { ...rest }> <LayoutGridItem className={`avatar-parts ${partItem.isSelected ? 'part-selected' : ''}`} itemImage={partItem.isClear ? undefined : imageUrl} {...rest}>
{ !hcDisabled && partItem.isHC && <i className="icon hc-icon position-absolute" /> } { !hcDisabled && partItem.isHC && <i className="icon hc-icon position-absolute" /> }
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> } { partItem.isClear && <AvatarEditorIcon icon="clear" /> }
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> } { partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
@ -34,4 +117,4 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
</LayoutGridItem> </LayoutGridItem>
</div> </div>
); );
} };

View File

@ -1,5 +1,5 @@
import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer'; 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 { FaTimes } from 'react-icons/fa';
import { CameraPicture, CreateLinkEvent, GetRoomEngine, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api'; import { CameraPicture, CreateLinkEvent, GetRoomEngine, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api';
import { Column, DraggableWindowCamera, Flex } from '../../../common'; import { Column, DraggableWindowCamera, Flex } from '../../../common';
@ -20,6 +20,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera(); const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera();
const { simpleAlert = null } = useNotification(); const { simpleAlert = null } = useNotification();
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
const [ isCapturing, setIsCapturing ] = useState(false);
const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null); const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null);
@ -32,7 +33,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
return new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height)); 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) if(selectedPictureIndex > -1)
{ {
@ -40,22 +41,39 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
return; return;
} }
setIsCapturing(true);
try
{
const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds()); const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds());
const imageUrl = await TextureUtils.generateImageUrl(texture);
if (!imageUrl || typeof imageUrl !== 'string' || !imageUrl.startsWith('data:image/')) {
simpleAlert(LocalizeText('camera.error.body'));
return;
}
const clone = [ ...cameraRoll ]; const clone = [ ...cameraRoll ];
if(clone.length >= CAMERA_ROLL_LIMIT) if(clone.length >= CAMERA_ROLL_LIMIT)
{ {
simpleAlert(LocalizeText('camera.full.body')); simpleAlert(LocalizeText('camera.full.body'));
clone.pop(); clone.pop();
} }
PlaySound(SoundNames.CAMERA_SHUTTER); PlaySound(SoundNames.CAMERA_SHUTTER);
clone.push(new CameraPicture(texture, TextureUtils.generateImageUrl(texture))); clone.push(new CameraPicture(texture, imageUrl));
setCameraRoll(clone); setCameraRoll(clone);
} }
catch (error)
{
simpleAlert(LocalizeText('camera.error.body'));
}
finally
{
setIsCapturing(false);
}
}
return ( return (
<DraggableWindowCamera uniqueKey="nitro-camera-capture"> <DraggableWindowCamera uniqueKey="nitro-camera-capture">

View File

@ -46,15 +46,17 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
useMessageEvent<CameraStorageUrlMessageEvent>(CameraStorageUrlMessageEvent, event => useMessageEvent<CameraStorageUrlMessageEvent>(CameraStorageUrlMessageEvent, event =>
{ {
const parser = event.getParser(); const parser = event.getParser();
const cameraUrl = GetConfiguration<string>('camera.url');
const fullUrl = cameraUrl + '/' + parser.url;
setPictureUrl(GetConfiguration<string>('camera.url') + '/' + parser.url); setPictureUrl(fullUrl);
}); });
useMessageEvent<NotEnoughBalanceMessageEvent>(NotEnoughBalanceMessageEvent, event => useMessageEvent<NotEnoughBalanceMessageEvent>(NotEnoughBalanceMessageEvent, event =>
{ {
const parser = event.getParser(); 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')); 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<CameraWidgetCheckoutViewProps> = props
useEffect(() => useEffect(() =>
{ {
if(!base64Url) return; if(!base64Url) return;
GetRoomEngine().saveBase64AsScreenshot(base64Url); GetRoomEngine().saveBase64AsScreenshot(base64Url);
}, [ base64Url ]); }, [ base64Url ]);
@ -102,12 +103,15 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } /> <NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
<NitroCardContentView> <NitroCardContentView>
<Flex center> <Flex center>
{ (pictureUrl && pictureUrl.length) && { (pictureUrl && pictureUrl.length) ? (
<LayoutImage className="picture-preview border" imageUrl={ pictureUrl } /> } <LayoutImage className="picture-preview border" imageUrl={ pictureUrl } />
{ (!pictureUrl || !pictureUrl.length) && ) : base64Url ? (
<LayoutImage className="picture-preview border" imageUrl={ base64Url } />
) : (
<Flex center className="picture-preview border"> <Flex center className="picture-preview border">
<Text bold>{ LocalizeText('camera.loading') }</Text> <Text bold>{ LocalizeText('camera.loading') }</Text>
</Flex> } </Flex>
) }
</Flex> </Flex>
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2"> <Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
<Column size={ publishDisabled ? 10 : 6 } gap={ 1 }> <Column size={ publishDisabled ? 10 : 6 } gap={ 1 }>

View File

@ -39,23 +39,28 @@ export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = 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); const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
if (!roomObject) return; if (!roomObject) return '';
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID); return type === 'username' ? roomObject.model.getValue<string>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
} }
useEffect(() => { setImageIndex(currentIndex); }, [ currentIndex ]); useEffect(() =>
{
setImageIndex(currentIndex);
}, [ currentIndex, currentPhotos ]);
if(!currentImage) return null; if(!currentImage) return null;
const imageUrl = currentImage.w || '';
return ( return (
<Grid style={ { display: 'flex', flexDirection: 'column' } }> <Grid style={ { display: 'flex', flexDirection: 'column' } }>
<Flex center className="picture-preview border border-black" style={ currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {} }> <Flex center className="picture-preview border border-black" style={ imageUrl ? { backgroundImage: `url(${imageUrl})` } : {} }>
{ !currentImage.w && <Text bold>{ LocalizeText('camera.loading') }</Text> } { !imageUrl && <Text bold>{ LocalizeText('camera.loading') }</Text> }
</Flex> </Flex>
{ currentImage.m && currentImage.m.length && <Text center>{ currentImage.m }</Text> } { currentImage.m && currentImage.m.length && <Text center>{ currentImage.m }</Text> }
<Flex alignItems="center" justifyContent="between"> <Flex alignItems="center" justifyContent="between">
<Text> { new Date(currentImage.t * 1000).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' }) } </Text> <Text> { new Date(currentImage.t * 1000).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' }) } </Text>
<Text className="username" onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text> <Text className="username" onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') || 'Unknown' } </Text>
</Flex> </Flex>
{ (currentPhotos.length > 1) && { (currentPhotos.length > 1) &&
<Flex className="picture-preview-buttons"> <Flex className="picture-preview-buttons">

View File

@ -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 { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa'; import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
import ReactSlider from 'react-slider'; import ReactSlider from 'react-slider';
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, GetRoomCameraWidgetManager, LocalizeText } from '../../../../api'; 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 { Button, ButtonGroup, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView'; import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView';
import { ColorMatrixFilter } from '@pixi/filter-color-matrix';
export interface CameraWidgetEditorViewProps export interface CameraWidgetEditorViewProps
{ {
@ -26,6 +27,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]); const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]); const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
const [ isZoomed, setIsZoomed ] = useState(false); const [ isZoomed, setIsZoomed ] = useState(false);
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string | null>(null);
const getColorMatrixEffects = useMemo(() => const getColorMatrixEffects = useMemo(() =>
{ {
@ -52,12 +54,12 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1; if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
return selectedEffects.findIndex(effect => (effect.effect.name === name)); return selectedEffects.findIndex(effect => (effect.effect.name === name));
}, [ selectedEffects ]) }, [ selectedEffects ]);
const getCurrentEffectIndex = useMemo(() => const getCurrentEffectIndex = useMemo(() =>
{ {
return getSelectedEffectIndex(selectedEffectName) return getSelectedEffectIndex(selectedEffectName);
}, [ selectedEffectName, getSelectedEffectIndex ]) }, [ selectedEffectName, getSelectedEffectIndex ]);
const getCurrentEffect = useMemo(() => const getCurrentEffect = useMemo(() =>
{ {
@ -83,9 +85,66 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
}); });
}, [ getCurrentEffectIndex, setSelectedEffects ]); }, [ getCurrentEffectIndex, setSelectedEffects ]);
const getCurrentPictureUrl = useMemo(() => const applyEffectsWithFallback = async (texture: RenderTexture, effects: IRoomCameraWidgetSelectedEffect[], isZoomed: boolean): Promise<string | null> =>
{ {
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 ]); }, [ picture, selectedEffects, isZoomed ]);
const processAction = useCallback((type: string, effectName: string = null) => const processAction = useCallback((type: string, effectName: string = null) =>
@ -99,7 +158,9 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
onCancel(); onCancel();
return; return;
case 'checkout': case 'checkout':
onCheckout(getCurrentPictureUrl); if (currentPictureUrl) {
onCheckout(currentPictureUrl);
}
return; return;
case 'change_tab': case 'change_tab':
setCurrentTab(String(effectName)); setCurrentTab(String(effectName));
@ -145,7 +206,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
case 'download': { case 'download': {
const image = new Image(); const image = new Image();
image.src = getCurrentPictureUrl image.src = currentPictureUrl || '';
const newWindow = window.open(''); const newWindow = window.open('');
newWindow.document.write(image.outerHTML); newWindow.document.write(image.outerHTML);
@ -155,18 +216,32 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
setIsZoomed(!isZoomed); setIsZoomed(!isZoomed);
return; return;
} }
}, [ isZoomed, availableEffects, selectedEffectName, getCurrentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]); }, [ isZoomed, availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]);
useEffect(() => useEffect(() =>
{ {
const thumbnails: CameraPictureThumbnail[] = []; if (!picture || !picture.texture) {
setEffectsThumbnails([]);
for(const effect of availableEffects) return;
{
thumbnails.push(new CameraPictureThumbnail(effect.name, GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false).src));
} }
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); setEffectsThumbnails(thumbnails);
};
generateThumbnails().catch(error => {
setEffectsThumbnails([]);
});
}, [ picture, availableEffects ]); }, [ picture, availableEffects ]);
return ( return (
@ -185,7 +260,11 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
</Column> </Column>
<Column size={ 7 } justifyContent="between" overflow="hidden"> <Column size={ 7 } justifyContent="between" overflow="hidden">
<Column center> <Column center>
<LayoutImage imageUrl={ getCurrentPictureUrl } className="picture-preview" /> { currentPictureUrl ? (
<LayoutImage imageUrl={ currentPictureUrl } className="picture-preview" />
) : (
<Text center bold>{ LocalizeText('camera.loading.error') }</Text>
) }
{ selectedEffectName && { selectedEffectName &&
<Column center fullWidth gap={ 1 }> <Column center fullWidth gap={ 1 }>
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text> <Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
@ -195,7 +274,11 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
step={ 0.01 } step={ 0.01 }
value={ getCurrentEffect.alpha } value={ getCurrentEffect.alpha }
onChange={ event => setSelectedEffectAlpha(event) } onChange={ event => setSelectedEffectAlpha(event) }
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } /> renderThumb={ (props, state) => {
const { key, ...restProps } = props;
return <div key={ key } { ...restProps }>{ state.valueNow }</div>;
} }
/>
</Column> } </Column> }
</Column> </Column>
<Flex justifyContent="between"> <Flex justifyContent="between">
@ -203,7 +286,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
<Button onClick={ event => processAction('clear_effects') }> <Button onClick={ event => processAction('clear_effects') }>
<FaTrash className="fa-icon" /> <FaTrash className="fa-icon" />
</Button> </Button>
<Button onClick={ event => processAction('download') }> <Button onClick={ event => processAction('download') } disabled={ !currentPictureUrl }>
<FaSave className="fa-icon" /> <FaSave className="fa-icon" />
</Button> </Button>
<Button onClick={ event => processAction('zoom') }> <Button onClick={ event => processAction('zoom') }>
@ -215,7 +298,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
<Button onClick={ event => processAction('cancel') }> <Button onClick={ event => processAction('cancel') }>
{ LocalizeText('generic.cancel') } { LocalizeText('generic.cancel') }
</Button> </Button>
<Button onClick={ event => processAction('checkout') }> <Button onClick={ event => processAction('checkout') } disabled={ !currentPictureUrl }>
{ LocalizeText('camera.preview.button.text') } { LocalizeText('camera.preview.button.text') }
</Button> </Button>
</Flex> </Flex>

View File

@ -53,7 +53,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
return ( return (
<LayoutGridItem itemImage={ iconUrl } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemActive={ itemActive } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } { ...rest }> <LayoutGridItem itemImage={ iconUrl } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemActive={ itemActive } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } { ...rest }>
{ (offer.product.productType === ProductTypeEnum.ROBOT) && { (offer.product.productType === ProductTypeEnum.ROBOT) &&
<LayoutAvatarImageView figure={ offer.product.extraParam } headOnly={ true } direction={ 3 } /> } <LayoutAvatarImageView figure={ offer.product.extraParam } direction={ 2 } /> }
</LayoutGridItem> </LayoutGridItem>
); );
} }

View File

@ -1,24 +1,21 @@
import { MouseEventType } from "@nitrots/nitro-renderer"; import { MouseEventType } from "@nitrots/nitro-renderer";
import { FC, MouseEvent, PropsWithChildren, useState } from "react"; import { FC, MouseEvent, PropsWithChildren, useState } from "react";
import { import { attemptBotPlacement, IBotItem, UnseenItemCategory, } from "../../../../api";
attemptBotPlacement,
IBotItem,
UnseenItemCategory,
} from "../../../../api";
import { LayoutAvatarImageView, LayoutGridItem } from "../../../../common"; import { LayoutAvatarImageView, LayoutGridItem } from "../../../../common";
import { useInventoryBots, useInventoryUnseenTracker } from "../../../../hooks"; import { useInventoryBots, useInventoryUnseenTracker } from "../../../../hooks";
export const InventoryBotItemView: FC< export const InventoryBotItemView: FC<PropsWithChildren<{ botItem: IBotItem }>> = props =>
PropsWithChildren<{ botItem: IBotItem }> {
> = (props) => {
const { botItem = null, children = null, ...rest } = props; const { botItem = null, children = null, ...rest } = props;
const [isMouseDown, setMouseDown] = useState(false); const [ isMouseDown, setMouseDown ] = useState(false);
const { selectedBot = null, setSelectedBot = null } = useInventoryBots(); const { selectedBot = null, setSelectedBot = null } = useInventoryBots();
const { isUnseen = null } = useInventoryUnseenTracker(); const { isUnseen = null } = useInventoryUnseenTracker();
const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id); const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id);
const onMouseEvent = (event: MouseEvent) => { const onMouseEvent = (event: MouseEvent) =>
switch (event.type) { {
switch(event.type)
{
case MouseEventType.MOUSE_DOWN: case MouseEventType.MOUSE_DOWN:
setSelectedBot(botItem); setSelectedBot(botItem);
setMouseDown(true); setMouseDown(true);
@ -27,32 +24,20 @@ export const InventoryBotItemView: FC<
setMouseDown(false); setMouseDown(false);
return; return;
case MouseEventType.ROLL_OUT: case MouseEventType.ROLL_OUT:
if (!isMouseDown || selectedBot !== botItem) return; if(!isMouseDown || (selectedBot !== botItem)) return;
attemptBotPlacement(botItem); attemptBotPlacement(botItem);
return; return;
case "dblclick": case 'dblclick':
attemptBotPlacement(botItem); attemptBotPlacement(botItem);
return; return;
} }
}; };
return ( return (
<LayoutGridItem <LayoutGridItem itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } onDoubleClick={ onMouseEvent } { ...rest }>
itemActive={selectedBot === botItem} <LayoutAvatarImageView figure={ botItem.botData.figure } direction={ 2 } />
itemUnseen={unseen} { children }
onMouseDown={onMouseEvent}
onMouseUp={onMouseEvent}
onMouseOut={onMouseEvent}
onDoubleClick={onMouseEvent}
{...rest}
>
<LayoutAvatarImageView
figure={botItem.botData.figure}
direction={3}
headOnly={true}
/>
{children}
</LayoutGridItem> </LayoutGridItem>
); );
}; };

View File

@ -77,8 +77,7 @@
color: #fff; color: #fff;
width: 400px; width: 400px;
min-height: 400px; min-height: 400px;
max-height: 500px; max-height: 400px;
overflow-y: hidden;
border: 2px solid #ffd700; border: 2px solid #ffd700;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
@ -117,7 +116,7 @@
padding: 20px; padding: 20px;
position: relative; position: relative;
height: calc(100% - 34px); height: calc(100% - 34px);
overflow-y: hidden; overflow-y: auto; /* Changed to auto to allow scrolling */
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
@ -125,12 +124,9 @@
} }
.grid { .grid {
animation: scroll-credits 20s linear infinite; animation: scroll-credits 40s linear infinite;
text-align: center; text-align: center;
position: absolute;
width: 100%; width: 100%;
top: 0;
left: 0;
will-change: transform; will-change: transform;
} }
@ -196,18 +192,14 @@
@keyframes scroll-credits { @keyframes scroll-credits {
0% { 0% {
transform: translateY(100%); transform: translateY(100%);
opacity: 0;
} }
10% { 5% {
transform: translateY(80%); transform: translateY(50%);
opacity: 1;
} }
90% { 95% {
transform: translateY(-80%); transform: translateY(-110%);
opacity: 1;
} }
100% { 100% {
transform: translateY(-100%); transform: translateY(-150%);
opacity: 0;
} }
} }

View File

@ -57,18 +57,16 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
<Text>- DuckieTM (Re-Design)</Text> <Text>- DuckieTM (Re-Design)</Text>
<Text>- Jonas (Contributing)</Text> <Text>- Jonas (Contributing)</Text>
<Text>- Ohlucas (Sunset resources)</Text> <Text>- Ohlucas (Sunset resources)</Text>
<Text center bold small>v1.5.0</Text> <Text center bold small>v2.0.0</Text>
<Button fullWidth onClick={event => window.open('https://github.com/duckietm/Nitro-Cool-UI')}> <Button fullWidth onClick={event => window.open('https://github.com/duckietm/Nitro-Cool-UI')}>
Cool UI Git Cool UI Git
</Button> </Button>
</Column> </Column>
</Flex> </Flex>
</Column> </Column>
<Column size={12}> <Column size={12}>
<div className="credits-divider"></div> <div className="credits-divider"></div>
</Column> </Column>
<Column size={10}> <Column size={10}>
<Column alignItems="center" gap={1}> <Column alignItems="center" gap={1}>
<Text center bold fontSize={5}>Special Thanks</Text> <Text center bold fontSize={5}>Special Thanks</Text>
@ -80,6 +78,23 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
</Column> </Column>
</Column> </Column>
<div className="notification-frank"></div> <div className="notification-frank"></div>
<Column size={12}>
<div className="credits-divider"></div>
</Column>
<Column size={10}>
<Column alignItems="center" gap={1}>
<Text center bold fontSize={5}>License</Text>
<Text center small>
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.
</Text>
<Button fullWidth onClick={event => window.open('https://www.gnu.org/licenses/gpl-3.0')}>
View Full License
</Button>
</Column>
</Column>
</Grid> </Grid>
</LayoutNotificationCredits> </LayoutNotificationCredits>
); );

View File

@ -38,23 +38,24 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const [ songId, setSongId ] = useState<number>(-1); const [ songId, setSongId ] = useState<number>(-1);
const [ songName, setSongName ] = useState<string>(''); const [ songName, setSongName ] = useState<string>('');
const [ songCreator, setSongCreator ] = useState<string>(''); const [ songCreator, setSongCreator ] = useState<string>('');
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<HTMLImageElement | null>(null);
useSoundEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event => useSoundEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event =>
{ {
setSongId(event.id); setSongId(event.id);
}, (isJukeBox || isSongDisk)); }, (isJukeBox || isSongDisk));
useSoundEvent<NowPlayingEvent>(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event => useSoundEvent<SongInfoReceivedEvent>(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); const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(event.id);
if(!songInfo) return; if (!songInfo) return;
setSongName(songInfo.name); setSongName(songInfo.name || '');
setSongCreator(songInfo.creator); setSongCreator(songInfo.creator || '');
}, (isJukeBox || isSongDisk)); }, (isJukeBox || isSongDisk));
useEffect(() => useEffect(() =>
@ -76,32 +77,44 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
let furniIsSongDisk = false; let furniIsSongDisk = false;
let furniSongId = -1; let furniSongId = -1;
const roomObject = GetRoomEngine().getRoomObject( roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR ); const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const location = roomObject.getLocation(); const location = roomObject.getLocation();
if (location) { if (location) {
setItemLocation({ x: location.x, y: location.y, z: location.z, }); setItemLocation({ x: location.x, y: location.y, z: location.z });
} }
const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST); const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST);
if(isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController) if (isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)
{ {
canMove = true; canMove = true;
canRotate = !avatarInfo.isWallItem; 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; 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; try {
if (
avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY ||
(avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER && isValidController) ||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX && isValidController) ||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT && isValidController)
) {
canUse = true;
}
} catch (error) {
console.warn('Error checking usage policy:', error);
}
if(avatarInfo.extraParam) if (avatarInfo.extraParam)
{ {
if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI) try {
if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI)
{ {
const stuffData = (avatarInfo.stuffData as CrackableDataType); const stuffData = (avatarInfo.stuffData as CrackableDataType);
@ -110,39 +123,34 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
crackableHits = stuffData.hits; crackableHits = stuffData.hits;
crackableTarget = stuffData.target; crackableTarget = stuffData.target;
} }
else if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
{ {
const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist(); const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist();
if (playlist)
if(playlist)
{ {
furniSongId = playlist.nowPlayingSongId; furniSongId = playlist.nowPlayingSongId;
} }
furniIsJukebox = true; furniIsJukebox = true;
} }
else if (avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
{ {
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length)); furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length)) || -1;
furniIsSongDisk = true; furniIsSongDisk = true;
} }
if(godMode) if (godMode)
{ {
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length); const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
if(extraParam) if (extraParam)
{ {
const parts = extraParam.split('\t'); const parts = extraParam.split('\t');
for(const part of parts) for (const part of parts)
{ {
const value = part.split('='); const value = part.split('=');
if(value && (value.length === 2)) if (value && (value.length === 2))
{ {
furniKeyss.push(value[0]); furniKeyss.push(value[0]);
furniValuess.push(value[1]); furniValuess.push(value[1]);
@ -150,20 +158,23 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
} }
} }
} }
} 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); const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, (avatarInfo.isWallItem) ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
if(roomObject) if (roomObject)
{ {
const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES); const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA); 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); customKeyss.push(customVariable);
customValuess.push((furnitureData[customVariable]) || ''); customValuess.push((furnitureData[customVariable]) || '');
@ -172,11 +183,10 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = 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); setPickupMode(pickupMode);
setCanMove(canMove); setCanMove(canMove);
@ -196,16 +206,26 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
setIsSongDisk(furniIsSongDisk); setIsSongDisk(furniIsSongDisk);
setSongId(furniSongId); 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 ]); }, [ roomSession, avatarInfo ]);
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event => useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
{ {
const parser = event.getParser(); 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); setGroupName(parser.title);
}); });
@ -213,44 +233,36 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
useEffect(() => useEffect(() =>
{ {
const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(songId); const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(songId);
setSongName(songInfo && songInfo.name ? songInfo.name : '');
setSongName(songInfo?.name ?? ''); setSongCreator(songInfo && songInfo.creator ? songInfo.creator : '');
setSongCreator(songInfo?.creator ?? '');
}, [ songId ]); }, [ songId ]);
const onFurniSettingChange = useCallback((index: number, value: string) => const onFurniSettingChange = useCallback((index: number, value: string) =>
{ {
const clone = Array.from(furniValues); const clone = Array.from(furniValues);
clone[index] = value; clone[index] = value;
setFurniValues(clone); setFurniValues(clone);
}, [ furniValues ]); }, [ furniValues ]);
const onCustomVariableChange = useCallback((index: number, value: string) => const onCustomVariableChange = useCallback((index: number, value: string) =>
{ {
const clone = Array.from(customValues); const clone = Array.from(customValues);
clone[index] = value; clone[index] = value;
setCustomValues(clone); setCustomValues(clone);
}, [ customValues ]); }, [ customValues ]);
const getFurniSettingsAsString = useCallback(() => const getFurniSettingsAsString = useCallback(() =>
{ {
if(furniKeys.length === 0 || furniValues.length === 0) return ''; if (furniKeys.length === 0 || furniValues.length === 0) return '';
let data = ''; let data = '';
let i = 0; let i = 0;
while(i < furniKeys.length) while (i < furniKeys.length)
{ {
const key = furniKeys[i]; const key = furniKeys[i];
const value = furniValues[i]; const value = furniValues[i];
data = (data + (key + '=' + value + '\t')); data = (data + (key + '=' + value + '\t'));
i++; i++;
} }
@ -259,11 +271,11 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const processButtonAction = useCallback((action: string) => const processButtonAction = useCallback((action: string) =>
{ {
if(!action || (action === '')) return; if (!action || (action === '')) return;
let objectData: string = null; let objectData: string = null;
switch(action) switch (action)
{ {
case 'buy_one': case 'buy_one':
CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`); CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`);
@ -275,7 +287,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE); GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
break; break;
case 'pickup': case 'pickup':
if(pickupMode === PICKUP_MODE_FULL) if (pickupMode === PICKUP_MODE_FULL)
{ {
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP); GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP);
} }
@ -291,12 +303,11 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
const mapData = new Map<string, string>(); const mapData = new Map<string, string>();
const dataParts = getFurniSettingsAsString().split('\t'); 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); const [ key, value ] = part.split('=', 2);
mapData.set(key, value); mapData.set(key, value);
} }
} }
@ -307,12 +318,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
case 'save_custom_variables': { case 'save_custom_variables': {
const map = new Map(); 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 key = customKeys[i];
const value = customValues[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)); SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map));
@ -325,12 +336,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ {
const stringDataType = (avatarInfo.stuffData as StringDataType); const stringDataType = (avatarInfo.stuffData as StringDataType);
if(!stringDataType || !(stringDataType instanceof StringDataType)) return null; if (!stringDataType || !(stringDataType instanceof StringDataType)) return null;
return stringDataType.getValue(2); return stringDataType.getValue(2);
}, [ avatarInfo ]); }, [ avatarInfo ]);
if(!avatarInfo) return null; if (!avatarInfo) return null;
return ( return (
<Column gap={ 1 } alignItems="end"> <Column gap={ 1 } alignItems="end">
@ -339,7 +350,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
<Column gap={ 1 }> <Column gap={ 1 }>
<Flex alignItems="center" justifyContent="between" gap={ 1 }> <Flex alignItems="center" justifyContent="between" gap={ 1 }>
{ !(isSongDisk) && <Text variant="white" wrap>{ avatarInfo.name }</Text> } { !(isSongDisk) && <Text variant="white" wrap>{ avatarInfo.name }</Text> }
{ (songName.length > 0) && <Text variant="white" wrap>{ songName }</Text> } { songName && songName.length > 0 && <Text variant="white" wrap>{ songName }</Text> }
<i className="infostand-close" onClick={ onClose } /> <i className="infostand-close" onClick={ onClose } />
</Flex> </Flex>
<hr className="m-0" /> <hr className="m-0" />
@ -354,8 +365,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
<div className="position-absolute end-0"> <div className="position-absolute end-0">
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } /> <LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
</div> } </div> }
{ avatarInfo.image && avatarInfo.image.src.length && { furniImage && furniImage.src && typeof furniImage.src === 'string' && furniImage.src.length > 0 ?
<img className="d-block mx-auto" src={ avatarInfo.image.src } alt="" /> } <>
<img className="d-block mx-auto" src={ furniImage.src } alt="" />
</> :
console.log('Skipping furni image: Invalid or missing src', furniImage)
}
</Flex> </Flex>
<hr className="m-0" /> <hr className="m-0" />
</Column> </Column>
@ -384,14 +399,14 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
<Text variant="white" small wrap> <Text variant="white" small wrap>
{ LocalizeText('infostand.jukebox.text.not.playing') } { LocalizeText('infostand.jukebox.text.not.playing') }
</Text> } </Text> }
{ !!songName.length && { songName && songName.length > 0 &&
<Flex alignItems="center" gap={ 1 }> <Flex alignItems="center" gap={ 1 }>
<Base className="icon disk-icon" /> <Base className="icon disk-icon" />
<Text variant="white" small wrap> <Text variant="white" small wrap>
{ songName } { songName }
</Text> </Text>
</Flex> } </Flex> }
{ !!songCreator.length && { songCreator && songCreator.length > 0 &&
<Flex alignItems="center" gap={ 1 }> <Flex alignItems="center" gap={ 1 }>
<Base className="icon disk-creator" /> <Base className="icon disk-creator" />
<Text variant="white" small wrap> <Text variant="white" small wrap>

View File

@ -157,7 +157,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
<Flex gap={ 1 }> <Flex gap={ 1 }>
<Column position="relative" pointer fullWidth className={ `body-image profile-background ${ infostandBackgroundClass }` } onClick={ event => GetUserProfile(avatarInfo.webID) }> <Column position="relative" pointer fullWidth className={ `body-image profile-background ${ infostandBackgroundClass }` } onClick={ event => GetUserProfile(avatarInfo.webID) }>
<Base position="absolute" className={ `body-image profile-stand ${ infostandStandClass }` }/> <Base position="absolute" className={ `body-image profile-stand ${ infostandStandClass }` }/>
<LayoutAvatarImageView figure={ avatarInfo.figure } direction={ 4 } /> <LayoutAvatarImageView figure={ avatarInfo.figure } direction={ 2 } style={{ position: 'relative', top: '-10px' }} />
<Base position="absolute" className={ `body-image profile-overlay ${ infostandOverlayClass }` }/> <Base position="absolute" className={ `body-image profile-overlay ${ infostandOverlayClass }` }/>
{ avatarInfo.type === AvatarInfoUser.OWN_USER && { avatarInfo.type === AvatarInfoUser.OWN_USER &&
<Base position="absolute" className="icon edit-icon edit-icon-position" onClick={ event => <Base position="absolute" className="icon edit-icon edit-icon-position" onClick={ event =>

View File

@ -1,6 +1,6 @@
import { MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react'; 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 { Base, Flex, LayoutItemCountView } from '../../common';
import { GuideToolEvent } from '../../events'; import { GuideToolEvent } from '../../events';

View File

@ -22,38 +22,39 @@
}, },
"main": "./index", "main": "./index",
"dependencies": { "dependencies": {
"@pixi/app": "~6.5.10", "@pixi/app": "~7.4.3",
"@pixi/basis": "~6.5.10", "@pixi/assets": "~7.4.3",
"@pixi/canvas-display": "~6.5.10", "@pixi/basis": "~7.4.3",
"@pixi/canvas-extract": "~6.5.10", "@pixi/canvas-display": "~7.4.3",
"@pixi/canvas-renderer": "~6.5.10", "@pixi/canvas-extract": "~7.4.3",
"@pixi/constants": "~6.5.10", "@pixi/canvas-renderer": "~7.4.3",
"@pixi/core": "~6.5.10", "@pixi/constants": "~7.4.3",
"@pixi/display": "~6.5.10", "@pixi/core": "~7.4.3",
"@pixi/events": "~6.5.10", "@pixi/display": "~7.4.3",
"@pixi/extensions": "~6.5.10", "@pixi/events": "~7.4.3",
"@pixi/extract": "~6.5.10", "@pixi/extensions": "~7.4.3",
"@pixi/filter-alpha": "~6.5.10", "@pixi/extract": "~7.4.3",
"@pixi/filter-color-matrix": "~6.5.10", "@pixi/filter-alpha": "~7.4.3",
"@pixi/graphics": "~6.5.10", "@pixi/filter-color-matrix": "~7.4.3",
"@pixi/graphics-extras": "~6.5.10", "@pixi/graphics": "~7.4.3",
"@pixi/graphics-extras": "~7.4.3",
"@pixi/interaction": "~6.5.10", "@pixi/interaction": "~6.5.10",
"@pixi/loaders": "~6.5.10", "@pixi/loaders": "~6.5.10",
"@pixi/math": "~6.5.10", "@pixi/math": "~7.4.3",
"@pixi/math-extras": "~6.5.10", "@pixi/math-extras": "~7.4.3",
"@pixi/mixin-cache-as-bitmap": "~6.5.10", "@pixi/mixin-cache-as-bitmap": "~7.4.3",
"@pixi/mixin-get-child-by-name": "~6.5.10", "@pixi/mixin-get-child-by-name": "~7.4.3",
"@pixi/mixin-get-global-position": "~6.5.10", "@pixi/mixin-get-global-position": "~7.4.3",
"@pixi/polyfill": "~6.5.10", "@pixi/polyfill": "~6.5.10",
"@pixi/runner": "~6.5.10", "@pixi/runner": "~7.4.3",
"@pixi/settings": "~6.5.10", "@pixi/settings": "~7.4.3",
"@pixi/sprite": "~6.5.10", "@pixi/sprite": "~7.4.3",
"@pixi/sprite-tiling": "~6.5.10", "@pixi/sprite-tiling": "~7.4.3",
"@pixi/spritesheet": "~6.5.10", "@pixi/spritesheet": "~7.4.3",
"@pixi/text": "~6.5.10", "@pixi/text": "~7.4.3",
"@pixi/ticker": "~6.5.10", "@pixi/ticker": "~7.4.3",
"@pixi/tilemap": "^3.2.2", "@pixi/tilemap": "^5.0.1",
"@pixi/utils": "~6.5.10", "@pixi/utils": "~7.4.3",
"clientjs": "^0.2.1", "clientjs": "^0.2.1",
"gifuct-js": "^2.1.2", "gifuct-js": "^2.1.2",
"howler": "^2.2.3", "howler": "^2.2.3",
@ -68,7 +69,7 @@
"@typescript-eslint/parser": "^5.30.7", "@typescript-eslint/parser": "^5.30.7",
"eslint": "^8.20.0", "eslint": "^8.20.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "~4.4.4", "typescript": "~5.4.5",
"vite": "^4.0.2", "vite": "^4.0.2",
"vite-plugin-minify": "^1.5.2" "vite-plugin-minify": "^1.5.2"
} }

View File

@ -17,7 +17,7 @@ export interface IAvatarImage extends IDisposable
getLayerData(_arg_1: ISpriteDataContainer): IAnimationLayerData; getLayerData(_arg_1: ISpriteDataContainer): IAnimationLayerData;
getImage(setType: string, hightlight: boolean, scale?: number, cache?: boolean): RenderTexture; getImage(setType: string, hightlight: boolean, scale?: number, cache?: boolean): RenderTexture;
getImageAsSprite(setType: string, scale?: number): Sprite; getImageAsSprite(setType: string, scale?: number): Sprite;
getCroppedImage(setType: string, scale?: number): HTMLImageElement; getCroppedImage(setType: string, scale?: number): Promise<HTMLImageElement>; // Ensure async
getAsset(_arg_1: string): IGraphicAsset; getAsset(_arg_1: string): IGraphicAsset;
getDirection(): number; getDirection(): number;
getFigure(): IAvatarFigureContainer; getFigure(): IAvatarFigureContainer;

View File

@ -1,6 +1,6 @@
export class NitroVersion 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 UI_VERSION: string = '';
public static sayHello(): void public static sayHello(): void

View File

@ -1,6 +1,5 @@
import '@pixi/canvas-display'; import '@pixi/canvas-display';
import { BatchRenderer, extensions } from '@pixi/core'; import { extensions } from '@pixi/core';
import { Extract } from '@pixi/extract';
import '@pixi/graphics-extras'; import '@pixi/graphics-extras';
import { AppLoaderPlugin } from '@pixi/loaders'; import { AppLoaderPlugin } from '@pixi/loaders';
import '@pixi/math-extras'; import '@pixi/math-extras';
@ -9,13 +8,8 @@ import '@pixi/mixin-get-child-by-name';
import '@pixi/mixin-get-global-position'; import '@pixi/mixin-get-global-position';
import '@pixi/polyfill'; import '@pixi/polyfill';
import { TilingSpriteRenderer } from '@pixi/sprite-tiling'; import { TilingSpriteRenderer } from '@pixi/sprite-tiling';
import { SpritesheetLoader } from '@pixi/spritesheet';
import { TickerPlugin } from '@pixi/ticker'; import { TickerPlugin } from '@pixi/ticker';
extensions.add( extensions.add(
BatchRenderer, TilingSpriteRenderer
Extract, );
TilingSpriteRenderer,
SpritesheetLoader,
AppLoaderPlugin,
TickerPlugin);

View File

@ -504,58 +504,60 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener
return container; return container;
} }
public getCroppedImage(setType: string, scale: number = 1): HTMLImageElement public async getCroppedImage(setType: string, scale: number = 1): Promise<HTMLImageElement>
{ {
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); 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 setTypes = this.getBodyParts(setType, this._mainAction.definition.geometryType, this._mainDirection);
const container = new NitroContainer(); 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 set = setTypes[partCount];
const part = this._cache.getImageContainer(set, this._frameCounter); const part = this._cache.getImageContainer(set, this._frameCounter);
if(part) if (part)
{ {
const partCacheContainer = part.image; const partCacheContainer = part.image;
if(!partCacheContainer) if (!partCacheContainer) {
{ console.warn(`getCroppedImage: No image for part ${set}`);
container.destroy({ container.destroy({ children: true });
children: true
});
return null; return null;
} }
const point = part.regPoint.clone(); const point = part.regPoint.clone();
if(point) if (point)
{ {
point.x += avatarCanvas.offset.x; point.x += avatarCanvas.offset.x;
point.y += avatarCanvas.offset.y; point.y += avatarCanvas.offset.y;
point.x += avatarCanvas.regPoint.x; point.x += avatarCanvas.regPoint.x;
point.y += avatarCanvas.regPoint.y; point.y += avatarCanvas.regPoint.y;
const partContainer = new NitroContainer(); const partContainer = new NitroContainer();
partContainer.addChild(partCacheContainer); partContainer.addChild(partCacheContainer);
if(partContainer) if (partContainer)
{ {
partContainer.position.set(point.x, point.y); partContainer.position.set(point.x, point.y);
container.addChild(partContainer); 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 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; return image;
} }

View File

@ -25,7 +25,11 @@ export class RenderRoomMessageComposer implements IMessageComposer<ConstructorPa
{ {
const url = TextureUtils.generateImageUrl(texture); const url = TextureUtils.generateImageUrl(texture);
if(!url) return; if(!url || typeof url !== 'string' || !url.startsWith('data:image/'))
{
console.warn('RenderRoomMessageComposer: Invalid or missing image URL', { url });
return;
}
const base64Data = url.split(',')[1]; const base64Data = url.split(',')[1];
const binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); const binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
@ -35,6 +39,12 @@ export class RenderRoomMessageComposer implements IMessageComposer<ConstructorPa
public assignBase64(base64: string): void public assignBase64(base64: string): void
{ {
if(!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/'))
{
console.warn('RenderRoomMessageComposer: Invalid or missing base64 data', { base64 });
return;
}
const base64Data = base64.split(',')[1]; const base64Data = base64.split(',')[1];
const binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); const binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));

View File

@ -37,7 +37,7 @@ export class FurnitureChangeStateWhenStepOnLogic extends FurnitureLogic
let sizeX = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_SIZE_X); let sizeX = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_SIZE_X);
let sizeY = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_SIZE_Y); let sizeY = this.object.model.getValue<number>(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]; if((direction === 1) || (direction === 3)) [sizeX, sizeY] = [sizeY, sizeX];

View File

@ -14,34 +14,48 @@ export class FurnitureDynamicThumbnailVisualization extends IsometricImageFurniV
this._hasOutline = true; this._hasOutline = true;
} }
protected updateModel(scale: number): boolean { protected async updateModel(scale: number): Promise<boolean>
if (this.object) { {
if (this.object)
{
const thumbnailUrl = this.getThumbnailURL(); const thumbnailUrl = this.getThumbnailURL();
if (this._cachedUrl !== thumbnailUrl) { if (this._cachedUrl !== thumbnailUrl)
{
this._cachedUrl = thumbnailUrl; this._cachedUrl = thumbnailUrl;
if (this._cachedUrl && this._cachedUrl !== '') { if (this._cachedUrl && this._cachedUrl !== '')
{
const image = new Image(); const image = new Image();
image.src = thumbnailUrl; image.src = thumbnailUrl;
image.crossOrigin = '*'; image.crossOrigin = '*';
await new Promise<void>((resolve) => {
image.onload = () => { image.onload = () => {
const texture = Texture.from(image); const texture = Texture.from(image);
texture.baseTexture.scaleMode = SCALE_MODES.LINEAR; texture.baseTexture.scaleMode = SCALE_MODES.LINEAR;
this.setThumbnailImages(texture); this.setThumbnailImages(texture);
resolve();
}; };
} else { image.onerror = () => {
console.warn('FurnitureDynamicThumbnailVisualization: Failed to load thumbnail image', { thumbnailUrl });
this.setThumbnailImages(null);
resolve();
};
});
}
else
{
this.setThumbnailImages(null); this.setThumbnailImages(null);
} }
} }
} }
return super.updateModel(scale); return await super.updateModel(scale);
} }
protected getThumbnailURL(): string protected getThumbnailURL(): string
{ {

View File

@ -12,6 +12,8 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
private _thumbnailImageNormal: Texture<Resource>; private _thumbnailImageNormal: Texture<Resource>;
private _thumbnailDirection: number; private _thumbnailDirection: number;
private _thumbnailChanged: boolean; private _thumbnailChanged: boolean;
private _uniqueId: string;
private _photoUrl: string;
protected _hasOutline: boolean; protected _hasOutline: boolean;
constructor() constructor()
@ -22,7 +24,9 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
this._thumbnailImageNormal = null; this._thumbnailImageNormal = null;
this._thumbnailDirection = -1; this._thumbnailDirection = -1;
this._thumbnailChanged = false; 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 public get hasThumbnailImage(): boolean
@ -30,58 +34,83 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
return !(this._thumbnailImageNormal == null); return !(this._thumbnailImageNormal == null);
} }
public setThumbnailImages(texture: Texture<Resource>): void public setThumbnailImages(k: Texture<Resource>, url?: string): void
{ {
this._thumbnailImageNormal = texture; this._thumbnailImageNormal = k;
this._photoUrl = url || null;
this._thumbnailChanged = true; 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<boolean>
{
const flag = await super.updateModel(scale);
this.refreshThumbnail(); if (!this._thumbnailChanged && (this._thumbnailDirection === this.direction)) {
return flag;
}
await this.refreshThumbnail();
return true; return true;
} }
private refreshThumbnail(): void private async refreshThumbnail(): Promise<void>
{ {
if(this.asset == null) return; if (this.asset == null) {
return;
if(this._thumbnailImageNormal)
{
this.addThumbnailAsset(this._thumbnailImageNormal, 64);
} }
else
{ const thumbnailAssetName = this.getThumbnailAssetName(64);
this.asset.disposeAsset(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._thumbnailChanged = false;
this._thumbnailDirection = this.direction; this._thumbnailDirection = this.direction;
} }
private addThumbnailAsset(texture: Texture<Resource>, scale: number): void private async addThumbnailAsset(k: Texture<Resource>, scale: number): Promise<void>
{ {
let layerId = 0; 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 assetName = (this.cacheSpriteAssetName(scale, layerId, false) + this.getFrameNumber(scale, layerId));
const asset = this.getAsset(assetName, 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) if (!transformedTexture) {
{ console.warn('IsometricImageFurniVisualization: Failed to generate transformed thumbnail for asset', { assetName });
const _local_6 = this.generateTransformedThumbnail(texture, asset); return;
const _local_7 = this.getThumbnailAssetName(scale); }
this.asset.disposeAsset(_local_7); const baseOffsetX = asset?.offsetX || 0;
this.asset.addAsset(_local_7, _local_6, true, asset.offsetX, asset.offsetY, false, false); 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; return;
@ -91,67 +120,75 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
} }
} }
protected generateTransformedThumbnail(texture: Texture<Resource>, asset: IGraphicAsset): Texture<Resource> protected async generateTransformedThumbnail(texture: Texture<Resource>, asset: IGraphicAsset): Promise<Texture<Resource>>
{ {
if(this._hasOutline)
{
const container = new NitroSprite();
const background = new NitroSprite(NitroTexture.WHITE);
background.tint = 0x000000;
background.width = (texture.width + 40);
background.height = (texture.height + 40);
const sprite = new NitroSprite(texture); const sprite = new NitroSprite(texture);
const offsetX = ((background.width - sprite.width) / 2);
const offsetY = ((background.height - sprite.height) / 2);
sprite.position.set(offsetX, offsetY); const photoContainer = new NitroSprite();
sprite.position.set(0, 0);
container.addChild(background, sprite); photoContainer.addChild(sprite);
texture = TextureUtils.generateTexture(container);
}
texture.orig.width = asset.width;
texture.orig.height = asset.height;
const scaleFactor = (asset?.width || 64) / texture.width;
const matrix = new Matrix(); const matrix = new Matrix();
switch(this.direction) switch (this.direction) {
{
case 2: case 2:
matrix.b = -(0.5); matrix.a = scaleFactor;
matrix.d /= 1.6; matrix.b = (-0.5 * scaleFactor);
matrix.ty = ((0.5) * texture.width); matrix.c = 0;
matrix.d = scaleFactor;
matrix.tx = 0;
matrix.ty = (0.5 * scaleFactor * texture.width);
break; break;
case 0: case 0:
case 4: case 4:
matrix.b = (0.5); matrix.a = scaleFactor;
matrix.d /= 1.6; matrix.b = (0.5 * scaleFactor);
matrix.tx = -0.5; matrix.c = 0;
matrix.d = scaleFactor;
matrix.tx = 0;
matrix.ty = 0;
break; 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 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); return super.getSpriteAssetName(scale, layerId);
} }
protected getThumbnailAssetName(scale: number): string protected getThumbnailAssetName(scale: number): string
{ {
this._thumbnailAssetNameNormal = this.getFullThumbnailAssetName(this.object.id, 64); return this.cacheSpriteAssetName(scale, 2, false) + this.getFrameNumber(scale, 2);
return this._thumbnailAssetNameNormal;
} }
protected getFullThumbnailAssetName(k: number, _arg_2: number): string protected getFullThumbnailAssetName(k: number, _arg_2: number): string

View File

@ -12,31 +12,42 @@ export class PlaneTextureCache
public RENDER_TEXTURE_POOL: Map<string, RenderTexture> = new Map(); public RENDER_TEXTURE_POOL: Map<string, RenderTexture> = new Map();
public RENDER_TEXTURE_CACHE: RenderTexture[] = []; 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 public clearCache(): void
{ {
this.RENDER_TEXTURE_POOL.forEach(renderTexture => renderTexture?.destroy(true)); this.RENDER_TEXTURE_POOL.forEach(renderTexture => renderTexture?.destroy(true));
this.RENDER_TEXTURE_POOL.clear(); this.RENDER_TEXTURE_POOL.clear();
this.RENDER_TEXTURE_CACHE = []; this.RENDER_TEXTURE_CACHE = [];
} }
public clearRenderTexture(renderTexture: RenderTexture): RenderTexture public clearRenderTexture(renderTexture: RenderTexture): RenderTexture
{ {
if(!renderTexture) return null; if (!renderTexture) return null;
return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture); return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture);
} }
private getTextureIdentifier(width: number, height: number, planeId: string): string 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 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({ const renderTexture = RenderTexture.create({
width, width,
@ -52,7 +63,7 @@ export class PlaneTextureCache
let renderTexture = this.RENDER_TEXTURE_POOL.get(planeId); let renderTexture = this.RENDER_TEXTURE_POOL.get(planeId);
if(!renderTexture) if (!renderTexture)
{ {
renderTexture = RenderTexture.create({ renderTexture = RenderTexture.create({
width, width,
@ -60,7 +71,6 @@ export class PlaneTextureCache
}); });
this.RENDER_TEXTURE_CACHE.push(renderTexture); this.RENDER_TEXTURE_CACHE.push(renderTexture);
this.RENDER_TEXTURE_POOL.set(planeId, 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 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); 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 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); const renderTexture = this.createRenderTexture(width, height, planeId);
@ -87,12 +97,11 @@ export class PlaneTextureCache
public clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture public clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture
{ {
if(!renderTexture) return null; if (!renderTexture) return null;
const sprite = new Sprite(Texture.WHITE); const sprite = new Sprite(Texture.WHITE);
sprite.tint = color; sprite.tint = color;
sprite.width = renderTexture.width; sprite.width = renderTexture.width;
sprite.height = renderTexture.height; sprite.height = renderTexture.height;
@ -101,7 +110,7 @@ export class PlaneTextureCache
public writeToRenderTexture(displayObject: DisplayObject, renderTexture: RenderTexture, clear: boolean = true, transform: Matrix = null): RenderTexture 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, { this.getRenderer().render(displayObject, {
renderTexture, renderTexture,
@ -114,7 +123,7 @@ export class PlaneTextureCache
public getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array public getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array
{ {
return this.getExtractor().pixels(displayObject); return this.getExtractor().pixels(displayObject, frame);
} }
public getRenderer(): Renderer | AbstractRenderer public getRenderer(): Renderer | AbstractRenderer
@ -124,6 +133,10 @@ export class PlaneTextureCache
public getExtractor(): Extract 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;
} }
} }

View File

@ -8,11 +8,57 @@ import { PixiApplicationProxy } from './PixiApplicationProxy';
export class TextureUtils 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<HTMLImageElement>
{
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 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, { return this.getRenderer().generateTexture(displayObject, {
scaleMode, scaleMode,
@ -23,42 +69,70 @@ export class TextureUtils
public static generateTextureFromImage(image: HTMLImageElement): Texture<Resource> public static generateTextureFromImage(image: HTMLImageElement): Texture<Resource>
{ {
if(!image) return null; if (!image) return null;
return Texture.from(image); return Texture.from(image);
} }
public static generateImage(target: DisplayObject | RenderTexture): HTMLImageElement public static async generateImageUrl(target: DisplayObject | RenderTexture): Promise<string>
{ {
if(!target) return null; if (!target) {
return null;
return this.getExtractor().image(target);
} }
public static generateImageUrl(target: DisplayObject | RenderTexture): string const extractor = this.getExtractor();
{ if (!extractor) {
if(!target) return null; return null;
}
return this.getExtractor().base64(target); let base64: string | Promise<string> = extractor.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 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 public static clearRenderTexture(renderTexture: RenderTexture): RenderTexture
{ {
if(!renderTexture) return null; if (!renderTexture) return null;
return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture); return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture);
} }
public static createRenderTexture(width: number, height: number): 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({ return RenderTexture.create({
width, width,
@ -68,7 +142,7 @@ export class TextureUtils
public static createAndFillRenderTexture(width: number, height: number, color: number = 16777215): RenderTexture 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); 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 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); const renderTexture = this.createRenderTexture(width, height);
@ -86,12 +160,11 @@ export class TextureUtils
public static clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture public static clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture
{ {
if(!renderTexture) return null; if (!renderTexture) return null;
const sprite = new Sprite(Texture.WHITE); const sprite = new Sprite(Texture.WHITE);
sprite.tint = color; sprite.tint = color;
sprite.width = renderTexture.width; sprite.width = renderTexture.width;
sprite.height = renderTexture.height; 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 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, { this.getRenderer().render(displayObject, {
renderTexture, renderTexture,
@ -113,16 +186,33 @@ export class TextureUtils
public static getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array 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 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 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;
} }
} }