mirror of
https://github.com/duckietm/Nitro-Cool-UI.git
synced 2025-06-21 22:36:58 +00:00
🆙 First Stage !
This commit is contained in:
parent
ba63e68b92
commit
a59c093e04
@ -21,7 +21,7 @@ $toolbar-height: 55px;
|
||||
$achievement-width: 375px;
|
||||
$achievement-height: 405px;
|
||||
|
||||
$avatar-editor-width: 520px;
|
||||
$avatar-editor-width: 545px;
|
||||
$avatar-editor-height: 553px;
|
||||
|
||||
$backgrounds-width: 534px;
|
||||
|
@ -1 +1 @@
|
||||
export const GetUIVersion = () => '2.1.1';
|
||||
export const GetUIVersion = () => '2.2.5';
|
||||
|
@ -3,6 +3,9 @@ import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { GetAvatarRenderManager } from '../../api';
|
||||
import { Base, BaseProps } from '../Base';
|
||||
|
||||
// Cache for avatar image URLs
|
||||
const AVATAR_IMAGE_CACHE: Map<string, { url: string }> = new Map();
|
||||
|
||||
export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
figure: string;
|
||||
@ -15,75 +18,166 @@ export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
|
||||
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
|
||||
{
|
||||
const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
|
||||
const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
|
||||
const [ randomValue, setRandomValue ] = useState(-1);
|
||||
const [ avatarUrl, setAvatarUrl ] = useState<string | null>(null);
|
||||
const [ isReady, setIsReady ] = useState<boolean>(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 newClassNames: string[] = [ 'avatar-image' ];
|
||||
|
||||
if(headOnly) newClassNames.push('head-only');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ classNames ]);
|
||||
}, [ classNames, headOnly ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
|
||||
if(avatarUrl && avatarUrl.length)
|
||||
{
|
||||
newStyle.backgroundImage = `url('${ avatarUrl }')`;
|
||||
}
|
||||
|
||||
newStyle.backgroundRepeat = 'no-repeat';
|
||||
newStyle.backgroundPosition = headOnly ? 'center -8px' : 'center';
|
||||
newStyle.position = 'relative';
|
||||
|
||||
if(scale !== 1)
|
||||
{
|
||||
newStyle.transform = `scale(${ scale })`;
|
||||
|
||||
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
|
||||
}
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ avatarUrl, scale, style ]);
|
||||
}, [ avatarUrl, scale, style, headOnly ]);
|
||||
|
||||
useEffect(() =>
|
||||
const loadAvatarImage = async () =>
|
||||
{
|
||||
if(isDisposed.current) return;
|
||||
|
||||
if(!figure || figure.length === 0)
|
||||
{
|
||||
setAvatarUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = AVATAR_IMAGE_CACHE.get(figureKey);
|
||||
if(cached)
|
||||
{
|
||||
setAvatarUrl(cached.url);
|
||||
retryCount.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
|
||||
resetFigure: figure =>
|
||||
resetFigure: figure =>
|
||||
{
|
||||
if(isDisposed.current) return;
|
||||
|
||||
setRandomValue(Math.random());
|
||||
loadAvatarImage();
|
||||
},
|
||||
dispose: () =>
|
||||
{},
|
||||
dispose: () => {},
|
||||
disposed: false
|
||||
}, null);
|
||||
|
||||
if(!avatarImage) return;
|
||||
|
||||
let setType = AvatarSetType.FULL;
|
||||
if(!avatarImage)
|
||||
{
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
setTimeout(loadAvatarImage, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAvatarUrl(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(headOnly) setType = AvatarSetType.HEAD;
|
||||
try
|
||||
{
|
||||
const setType = headOnly ? AvatarSetType.HEAD : AvatarSetType.FULL;
|
||||
avatarImage.setDirection(setType, direction);
|
||||
|
||||
avatarImage.setDirection(setType, direction);
|
||||
const image = await avatarImage.getCroppedImage(setType);
|
||||
|
||||
const image = avatarImage.getCroppedImage(setType);
|
||||
if(isDisposed.current) return;
|
||||
|
||||
if(image) setAvatarUrl(image.src);
|
||||
|
||||
avatarImage.dispose();
|
||||
}, [ figure, gender, direction, headOnly, randomValue ]);
|
||||
if(image && image.src && typeof image.src === 'string' && image.src.startsWith('data:image/'))
|
||||
{
|
||||
setAvatarUrl(image.src);
|
||||
AVATAR_IMAGE_CACHE.set(figureKey, { url: image.src });
|
||||
retryCount.current = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
setTimeout(loadAvatarImage, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAvatarUrl(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn(`LayoutAvatarImageView: Error loading avatar image`, error);
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
setTimeout(loadAvatarImage, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setAvatarUrl(null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
setTimeout(() => {
|
||||
if(!isDisposed.current) avatarImage.dispose();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
isDisposed.current = false;
|
||||
retryCount.current = 0;
|
||||
setIsReady(true);
|
||||
|
||||
loadAvatarImage();
|
||||
|
||||
const handleVisibilityChange = () =>
|
||||
{
|
||||
if(document.visibilityState === 'visible')
|
||||
{
|
||||
setAvatarUrl(null);
|
||||
loadAvatarImage();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () =>
|
||||
{
|
||||
isDisposed.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [ figure, gender, direction, headOnly, figureKey ]);
|
||||
|
||||
return <Base innerRef={ elementRef } classNames={ getClassNames } style={ getStyle } { ...rest } />;
|
||||
};
|
@ -23,9 +23,7 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
const newClassNames: string[] = [ 'badge-image' ];
|
||||
|
||||
if(isGroup) newClassNames.push('group-badge');
|
||||
|
||||
if(isGrayscale) newClassNames.push('grayscale');
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
@ -37,16 +35,16 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
|
||||
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.height = imageElement.height;
|
||||
|
||||
if(scale !== 1)
|
||||
{
|
||||
newStyle.transform = `scale(${ scale })`;
|
||||
|
||||
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
|
||||
|
||||
newStyle.width = (imageElement.width * scale);
|
||||
newStyle.height = (imageElement.height * scale);
|
||||
}
|
||||
@ -55,37 +53,81 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ imageElement, scale, style ]);
|
||||
}, [ badgeCode, imageElement, isGroup, scale, style ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!badgeCode || !badgeCode.length) return;
|
||||
|
||||
if(!badgeCode || !badgeCode.length)
|
||||
{
|
||||
console.warn('LayoutBadgeImageView: Invalid or empty badgeCode', badgeCode);
|
||||
setImageElement(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let didSetBadge = false;
|
||||
|
||||
const onBadgeImageReadyEvent = (event: BadgeImageReadyEvent) =>
|
||||
const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) =>
|
||||
{
|
||||
if(event.badgeId !== badgeCode) return;
|
||||
|
||||
const element = TextureUtils.generateImage(new NitroSprite(event.image));
|
||||
try
|
||||
{
|
||||
const sprite = new NitroSprite(event.image);
|
||||
const element = await TextureUtils.generateImage(sprite);
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
|
||||
didSetBadge = true;
|
||||
if(element && element.src && element.src.startsWith('data:image/'))
|
||||
{
|
||||
setImageElement(element);
|
||||
didSetBadge = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutBadgeImageView: Invalid badge image (event)', element);
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutBadgeImageView: Error generating badge image (event)', error);
|
||||
}
|
||||
|
||||
GetSessionDataManager().events.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
}
|
||||
};
|
||||
|
||||
GetSessionDataManager().events.addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
|
||||
const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode);
|
||||
|
||||
if(texture && !didSetBadge)
|
||||
const loadBadgeImage = async () =>
|
||||
{
|
||||
const element = TextureUtils.generateImage(new NitroSprite(texture));
|
||||
const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode);
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
}
|
||||
if(texture && !didSetBadge)
|
||||
{
|
||||
try
|
||||
{
|
||||
const sprite = new NitroSprite(texture);
|
||||
const element = await TextureUtils.generateImage(sprite);
|
||||
|
||||
if(element && element.src && element.src.startsWith('data:image/'))
|
||||
{
|
||||
setImageElement(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutBadgeImageView: Invalid badge image (direct)', element);
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutBadgeImageView: Error generating badge image (direct)', error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('LayoutBadgeImageView: No texture found for badge', badgeCode);
|
||||
}
|
||||
};
|
||||
|
||||
loadBadgeImage();
|
||||
|
||||
return () => GetSessionDataManager().events.removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
}, [ badgeCode, isGroup ]);
|
||||
@ -100,4 +142,4 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
{ children }
|
||||
</Base>
|
||||
);
|
||||
}
|
||||
};
|
@ -15,7 +15,7 @@ interface LayoutFurniImageViewProps extends BaseProps<HTMLDivElement>
|
||||
|
||||
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 getStyle = useMemo(() =>
|
||||
@ -24,15 +24,19 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
|
||||
|
||||
if(imageElement?.src?.length)
|
||||
{
|
||||
console.log('LayoutFurniImageView: Applying image URL', imageElement.src);
|
||||
newStyle.backgroundImage = `url('${ imageElement.src }')`;
|
||||
newStyle.width = imageElement.width;
|
||||
newStyle.height = imageElement.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log('LayoutFurniImageView: No imageElement, skipping style');
|
||||
}
|
||||
|
||||
if(scale !== 1)
|
||||
{
|
||||
newStyle.transform = `scale(${ scale })`;
|
||||
|
||||
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
|
||||
}
|
||||
|
||||
@ -43,40 +47,111 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
console.log('LayoutFurniImageView: productType:', productType, 'productClassId:', productClassId, 'direction:', direction, 'extraData:', extraData);
|
||||
|
||||
if(productClassId < 0 || !productType)
|
||||
{
|
||||
console.warn('LayoutFurniImageView: Invalid productClassId or productType', { productClassId, productType });
|
||||
setImageElement(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let imageResult: ImageResult = null;
|
||||
|
||||
const listener: IGetImageListener = {
|
||||
imageReady: (id, texture, image) =>
|
||||
imageReady: async (id, texture, image) =>
|
||||
{
|
||||
if(!image && texture)
|
||||
{
|
||||
image = TextureUtils.generateImage(texture);
|
||||
}
|
||||
console.log('LayoutFurniImageView: imageReady called', { id, texture, image });
|
||||
|
||||
image.onload = () => setImageElement(image);
|
||||
try
|
||||
{
|
||||
if(!image && texture)
|
||||
{
|
||||
image = await TextureUtils.generateImage(texture);
|
||||
console.log('LayoutFurniImageView: Generated image from texture', image?.src);
|
||||
}
|
||||
|
||||
if(image && image.src && image.src.startsWith('data:image/'))
|
||||
{
|
||||
image.onload = () => {
|
||||
console.log('LayoutFurniImageView: Image loaded', image.src);
|
||||
setImageElement(image);
|
||||
};
|
||||
if(image.complete) {
|
||||
console.log('LayoutFurniImageView: Image already complete', image.src);
|
||||
setImageElement(image);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutFurniImageView: Invalid image in imageReady', image);
|
||||
setImageElement(null);
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutFurniImageView: Error in imageReady', error);
|
||||
setImageElement(null);
|
||||
}
|
||||
},
|
||||
imageFailed: null
|
||||
imageFailed: (id) => {
|
||||
console.warn('LayoutFurniImageView: Image fetch failed for id', id);
|
||||
setImageElement(null);
|
||||
}
|
||||
};
|
||||
|
||||
switch(productType.toLocaleLowerCase())
|
||||
try
|
||||
{
|
||||
case ProductTypeEnum.FLOOR:
|
||||
imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
|
||||
break;
|
||||
case ProductTypeEnum.WALL:
|
||||
imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
|
||||
break;
|
||||
switch(productType.toLowerCase())
|
||||
{
|
||||
case ProductTypeEnum.FLOOR:
|
||||
console.log('LayoutFurniImageView: Fetching floor furniture image');
|
||||
imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
|
||||
break;
|
||||
case ProductTypeEnum.WALL:
|
||||
console.log('LayoutFurniImageView: Fetching wall furniture image');
|
||||
imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
|
||||
break;
|
||||
default:
|
||||
console.warn('LayoutFurniImageView: Unknown productType', productType);
|
||||
setImageElement(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(imageResult)
|
||||
{
|
||||
const image = imageResult.getImage();
|
||||
console.log('LayoutFurniImageView: Immediate imageResult', image?.src);
|
||||
|
||||
if(image && image.src && image.src.startsWith('data:image/'))
|
||||
{
|
||||
image.onload = () => {
|
||||
console.log('LayoutFurniImageView: Immediate image loaded', image.src);
|
||||
setImageElement(image);
|
||||
};
|
||||
if(image.complete) {
|
||||
console.log('LayoutFurniImageView: Immediate image already complete', image.src);
|
||||
setImageElement(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutFurniImageView: No imageResult returned');
|
||||
}
|
||||
}
|
||||
|
||||
if(imageResult)
|
||||
catch(error)
|
||||
{
|
||||
const image = imageResult.getImage();
|
||||
|
||||
image.onload = () => setImageElement(image);
|
||||
console.warn('LayoutFurniImageView: Error fetching image', error);
|
||||
setImageElement(null);
|
||||
}
|
||||
}, [ productType, productClassId, direction, extraData ]);
|
||||
|
||||
if(!imageElement) return null;
|
||||
if(!imageElement)
|
||||
{
|
||||
console.log('LayoutFurniImageView: Skipping render, no imageElement');
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />;
|
||||
}
|
||||
return <Base classNames={ [ 'furni-image', ...classNames ] } style={ getStyle } { ...rest } />;
|
||||
};
|
@ -14,41 +14,44 @@ interface LayoutPetImageViewProps extends BaseProps<HTMLDivElement>
|
||||
headOnly?: boolean;
|
||||
direction?: number;
|
||||
scale?: number;
|
||||
isIcon?: boolean;
|
||||
}
|
||||
|
||||
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 [ width, setWidth ] = useState(0);
|
||||
const [ height, setHeight ] = useState(0);
|
||||
const isDisposed = useRef(false);
|
||||
const retryCount = useRef(0);
|
||||
const maxRetries = 3;
|
||||
const prevFigure = useRef<string>('');
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
if(petUrl && petUrl.length) newStyle.backgroundImage = `url(${ petUrl })`;
|
||||
if(petUrl && petUrl.length)
|
||||
{
|
||||
newStyle.backgroundImage = `url(${ petUrl })`;
|
||||
newStyle.width = width;
|
||||
newStyle.height = height;
|
||||
}
|
||||
|
||||
if(scale !== 1)
|
||||
{
|
||||
newStyle.transform = `scale(${ scale })`;
|
||||
|
||||
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
|
||||
}
|
||||
|
||||
newStyle.width = width;
|
||||
newStyle.height = height;
|
||||
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ petUrl, scale, style, width, height ]);
|
||||
}, [ petUrl, scale, style, width, height, isIcon ]);
|
||||
|
||||
useEffect(() =>
|
||||
const fetchPetImage = () =>
|
||||
{
|
||||
let url = null;
|
||||
|
||||
let petTypeId = typeId;
|
||||
let petPaletteId = paletteId;
|
||||
let petColor1 = petColor;
|
||||
@ -57,65 +60,177 @@ export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
|
||||
|
||||
if(figure && figure.length)
|
||||
{
|
||||
const petFigureData = new PetFigureData(figure);
|
||||
|
||||
petTypeId = petFigureData.typeId;
|
||||
petPaletteId = petFigureData.paletteId;
|
||||
petColor1 = petFigureData.color;
|
||||
petCustomParts = petFigureData.customParts;
|
||||
try
|
||||
{
|
||||
const petFigureData = new PetFigureData(figure);
|
||||
petTypeId = petFigureData.typeId;
|
||||
petPaletteId = petFigureData.paletteId;
|
||||
petColor1 = petFigureData.color;
|
||||
petCustomParts = petFigureData.customParts;
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn(`LayoutPetImageView: Error parsing PetFigureData (isIcon: ${isIcon})`, error, 'Figure:', figure);
|
||||
setPetUrl(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(petTypeId === 16) petHeadOnly = false;
|
||||
|
||||
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, {
|
||||
imageReady: (id, texture, image) =>
|
||||
{
|
||||
if(isDisposed.current) return;
|
||||
|
||||
if(image)
|
||||
{
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
}
|
||||
|
||||
else if(texture)
|
||||
{
|
||||
setPetUrl(TextureUtils.generateImageUrl(texture));
|
||||
setWidth(texture.width);
|
||||
setHeight(texture.height);
|
||||
}
|
||||
},
|
||||
imageFailed: (id) =>
|
||||
{
|
||||
|
||||
}
|
||||
}, petHeadOnly, 0, petCustomParts, posture);
|
||||
|
||||
if(imageResult)
|
||||
if(petTypeId < 0 || petPaletteId < 0)
|
||||
{
|
||||
const image = imageResult.getImage();
|
||||
console.warn(`LayoutPetImageView: Invalid petTypeId or petPaletteId (isIcon: ${isIcon})`, { petTypeId, petPaletteId, figure });
|
||||
setPetUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(image)
|
||||
try
|
||||
{
|
||||
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), isIcon ? 32 : 64, {
|
||||
imageReady: async (id, texture, image) =>
|
||||
{
|
||||
if(isDisposed.current) return;
|
||||
|
||||
try
|
||||
{
|
||||
if(image && image.src && image.src.startsWith('data:image/'))
|
||||
{
|
||||
image.onload = () => {
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
};
|
||||
if(image.complete)
|
||||
{
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
}
|
||||
}
|
||||
else if(texture)
|
||||
{
|
||||
const url = await TextureUtils.generateImageUrl(texture);
|
||||
if(url && url.startsWith('data:image/'))
|
||||
{
|
||||
setPetUrl(url);
|
||||
setWidth(texture.width || (isIcon ? 32 : 64));
|
||||
setHeight(texture.height || (isIcon ? 32 : 64));
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn(`LayoutPetImageView: Invalid texture URL (isIcon: ${isIcon})`, url);
|
||||
setPetUrl(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn(`LayoutPetImageView: No image or texture in imageReady (isIcon: ${isIcon})`, { id });
|
||||
setPetUrl(null);
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn(`LayoutPetImageView: Error in imageReady (isIcon: ${isIcon})`, error, 'ID:', id);
|
||||
setPetUrl(null);
|
||||
}
|
||||
},
|
||||
imageFailed: (id) =>
|
||||
{
|
||||
console.warn(`LayoutPetImageView: Image fetch failed for id (isIcon: ${isIcon})`, id, 'Props:', { petTypeId, petPaletteId, posture });
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`);
|
||||
setTimeout(fetchPetImage, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setPetUrl(null);
|
||||
}
|
||||
}
|
||||
}, petHeadOnly, 0, petCustomParts, posture);
|
||||
|
||||
if(imageResult)
|
||||
{
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
(async () =>
|
||||
{
|
||||
const image = await imageResult.getImage();
|
||||
|
||||
if(image && image.src && image.src.startsWith('data:image/'))
|
||||
{
|
||||
image.onload = () => {
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
};
|
||||
if(image.complete)
|
||||
{
|
||||
setPetUrl(image.src);
|
||||
setWidth(image.width);
|
||||
setHeight(image.height);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`);
|
||||
setTimeout(fetchPetImage, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]);
|
||||
catch(error)
|
||||
{
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
console.log(`LayoutPetImageView: Retrying fetch (retry: ${retryCount.current}/${maxRetries})`);
|
||||
setTimeout(fetchPetImage, 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
setPetUrl(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
isDisposed.current = false;
|
||||
retryCount.current = 0;
|
||||
|
||||
// Force reload if figure changes
|
||||
const figureChanged = prevFigure.current !== figure;
|
||||
if(figureChanged)
|
||||
{
|
||||
setPetUrl(null);
|
||||
prevFigure.current = figure;
|
||||
}
|
||||
|
||||
fetchPetImage();
|
||||
|
||||
// Handle visibility changes
|
||||
const handleVisibilityChange = () =>
|
||||
{
|
||||
if(document.visibilityState === 'visible')
|
||||
{
|
||||
setPetUrl(null); // Reset to force reload
|
||||
fetchPetImage();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () =>
|
||||
{
|
||||
isDisposed.current = true;
|
||||
}
|
||||
}, []);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction, isIcon ]);
|
||||
|
||||
const url = `url('${ petUrl }')`;
|
||||
|
||||
return <Base classNames={ [ 'pet-image' ] } style={ getStyle } { ...rest } />;
|
||||
}
|
||||
return <Base classNames={ [ 'pet-image', isIcon ? 'pet-icon' : '', ...classNames ] } style={ getStyle } { ...rest } />;
|
||||
};
|
@ -24,43 +24,79 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!roomPreviewer) return;
|
||||
if(!roomPreviewer || height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const update = (time: number) =>
|
||||
const update = async (time: number) =>
|
||||
{
|
||||
if(!roomPreviewer || !renderingCanvas || !elementRef.current) return;
|
||||
|
||||
roomPreviewer.updatePreviewRoomView();
|
||||
|
||||
if(!renderingCanvas.canvasUpdated) return;
|
||||
try
|
||||
{
|
||||
roomPreviewer.updatePreviewRoomView();
|
||||
|
||||
elementRef.current.style.backgroundImage = `url(${ TextureUtils.generateImageUrl(renderingCanvas.master) })`;
|
||||
if(!renderingCanvas.canvasUpdated) return;
|
||||
|
||||
const imageUrl = await TextureUtils.generateImageUrl(renderingCanvas.master);
|
||||
|
||||
if(imageUrl && imageUrl.startsWith('data:image/'))
|
||||
{
|
||||
elementRef.current.style.backgroundImage = `url(${ imageUrl })`;
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Invalid image URL', imageUrl);
|
||||
elementRef.current.style.backgroundImage = '';
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Error updating preview', error);
|
||||
elementRef.current.style.backgroundImage = '';
|
||||
}
|
||||
}
|
||||
|
||||
if(!renderingCanvas)
|
||||
{
|
||||
if(elementRef.current && roomPreviewer)
|
||||
{
|
||||
const computed = document.defaultView.getComputedStyle(elementRef.current, null);
|
||||
try
|
||||
{
|
||||
const computed = document.defaultView.getComputedStyle(elementRef.current, null);
|
||||
let backgroundColor = computed.backgroundColor;
|
||||
|
||||
let backgroundColor = computed.backgroundColor;
|
||||
backgroundColor = ColorConverter.rgbStringToHex(backgroundColor);
|
||||
backgroundColor = backgroundColor.replace('#', '0x');
|
||||
|
||||
backgroundColor = ColorConverter.rgbStringToHex(backgroundColor);
|
||||
backgroundColor = backgroundColor.replace('#', '0x');
|
||||
roomPreviewer.backgroundColor = parseInt(backgroundColor, 16);
|
||||
|
||||
roomPreviewer.backgroundColor = parseInt(backgroundColor, 16);
|
||||
const width = elementRef.current.parentElement.clientWidth;
|
||||
|
||||
const width = elementRef.current.parentElement.clientWidth;
|
||||
|
||||
roomPreviewer.getRoomCanvas(width, height);
|
||||
roomPreviewer.getRoomCanvas(width, height);
|
||||
|
||||
const canvas = roomPreviewer.getRenderingCanvas();
|
||||
const canvas = roomPreviewer.getRenderingCanvas();
|
||||
|
||||
setRenderingCanvas(canvas);
|
||||
|
||||
canvas.canvasUpdated = true;
|
||||
|
||||
update(-1);
|
||||
if(canvas)
|
||||
{
|
||||
setRenderingCanvas(canvas);
|
||||
canvas.canvasUpdated = true;
|
||||
update(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Failed to initialize canvas');
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Error initializing canvas', error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Missing elementRef or roomPreviewer');
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,20 +108,24 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
|
||||
|
||||
const width = elementRef.current.parentElement.offsetWidth;
|
||||
|
||||
roomPreviewer.modifyRoomCanvas(width, height);
|
||||
|
||||
update(-1);
|
||||
try
|
||||
{
|
||||
roomPreviewer.modifyRoomCanvas(width, height);
|
||||
update(-1);
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn('LayoutRoomPreviewerView: Error resizing canvas', error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
resizeObserver.observe(elementRef.current);
|
||||
|
||||
return () =>
|
||||
{
|
||||
resizeObserver.disconnect();
|
||||
|
||||
GetTicker().remove(update);
|
||||
}
|
||||
|
||||
}, [ renderingCanvas, roomPreviewer, elementRef, height ]);
|
||||
|
||||
return (
|
||||
@ -94,4 +134,4 @@ export const LayoutRoomPreviewerView: FC<LayoutRoomPreviewerViewProps> = props =
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
@ -334,7 +334,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 125px;
|
||||
bottom: -125px;
|
||||
margin: 0 auto;
|
||||
z-index: 4;
|
||||
}
|
||||
@ -359,7 +359,7 @@
|
||||
}
|
||||
|
||||
.choose-clothing {
|
||||
width: 320px;
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.color-picker-frame {
|
||||
@ -477,9 +477,9 @@
|
||||
|
||||
.avatar-parts {
|
||||
border: none !important;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
background-position: center;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 2rem !important;
|
||||
overflow: visible !important;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState, useRef } from 'react';
|
||||
import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api';
|
||||
import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common';
|
||||
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common';
|
||||
import { AvatarEditorIcon } from '../AvatarEditorIcon';
|
||||
|
||||
export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps
|
||||
@ -12,21 +12,104 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
|
||||
{
|
||||
const { partItem = null, children = null, ...rest } = props;
|
||||
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 loadPartImage = async () =>
|
||||
{
|
||||
if(isDisposed.current) return;
|
||||
|
||||
if(!partItem)
|
||||
{
|
||||
setImageUrl(null);
|
||||
setIsValid(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedImageUrl: string | null = null;
|
||||
|
||||
if(partItem.imageUrl && typeof partItem.imageUrl === 'object' && 'then' in partItem.imageUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
resolvedImageUrl = await partItem.imageUrl;
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.warn(`AvatarEditorFigureSetItemView: Failed to resolve imageUrl promise for item ${partItem.id}`, error);
|
||||
}
|
||||
}
|
||||
else if(typeof partItem.imageUrl === 'string')
|
||||
{
|
||||
resolvedImageUrl = partItem.imageUrl;
|
||||
}
|
||||
|
||||
if(!resolvedImageUrl || !resolvedImageUrl.startsWith('data:image/'))
|
||||
{
|
||||
if(retryCount.current < maxRetries)
|
||||
{
|
||||
retryCount.current += 1;
|
||||
setTimeout(loadPartImage, 1500);
|
||||
}
|
||||
else
|
||||
{
|
||||
setImageUrl(null);
|
||||
setIsValid(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setImageUrl(resolvedImageUrl);
|
||||
setIsValid(true);
|
||||
retryCount.current = 0;
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const rerender = () => setUpdateId(prevValue => (prevValue + 1));
|
||||
isDisposed.current = false;
|
||||
retryCount.current = 0;
|
||||
setIsValid(true);
|
||||
|
||||
loadPartImage();
|
||||
|
||||
const rerender = () =>
|
||||
{
|
||||
setUpdateId(prevValue => (prevValue + 1));
|
||||
loadPartImage();
|
||||
};
|
||||
|
||||
partItem.notify = rerender;
|
||||
|
||||
return () => partItem.notify = null;
|
||||
const handleVisibilityChange = () =>
|
||||
{
|
||||
if(document.visibilityState === 'visible')
|
||||
{
|
||||
setImageUrl(null);
|
||||
setIsValid(true);
|
||||
loadPartImage();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () =>
|
||||
{
|
||||
isDisposed.current = true;
|
||||
partItem.notify = null;
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [ partItem ]);
|
||||
|
||||
if(!isValid) return null;
|
||||
|
||||
return (
|
||||
<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" /> }
|
||||
{ partItem.isClear && <AvatarEditorIcon icon="clear" /> }
|
||||
{ partItem.isSellable && <AvatarEditorIcon icon="sellable" position="absolute" className="end-1 bottom-1" /> }
|
||||
@ -34,4 +117,4 @@ export const AvatarEditorFigureSetItemView: FC<AvatarEditorFigureSetItemViewProp
|
||||
</LayoutGridItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { FC, useRef } from 'react';
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { CameraPicture, CreateLinkEvent, GetRoomEngine, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api';
|
||||
import { Column, DraggableWindowCamera, Flex } from '../../../common';
|
||||
@ -20,6 +20,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera();
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const [ isCapturing, setIsCapturing ] = useState(false);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const takePicture = () =>
|
||||
const takePicture = async () =>
|
||||
{
|
||||
if(selectedPictureIndex > -1)
|
||||
{
|
||||
@ -40,21 +41,38 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds());
|
||||
|
||||
const clone = [ ...cameraRoll ];
|
||||
|
||||
if(clone.length >= CAMERA_ROLL_LIMIT)
|
||||
setIsCapturing(true);
|
||||
try
|
||||
{
|
||||
simpleAlert(LocalizeText('camera.full.body'));
|
||||
const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds());
|
||||
const imageUrl = await TextureUtils.generateImageUrl(texture);
|
||||
|
||||
clone.pop();
|
||||
if (!imageUrl || typeof imageUrl !== 'string' || !imageUrl.startsWith('data:image/')) {
|
||||
simpleAlert(LocalizeText('camera.error.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
const clone = [ ...cameraRoll ];
|
||||
|
||||
if(clone.length >= CAMERA_ROLL_LIMIT)
|
||||
{
|
||||
simpleAlert(LocalizeText('camera.full.body'));
|
||||
clone.pop();
|
||||
}
|
||||
|
||||
PlaySound(SoundNames.CAMERA_SHUTTER);
|
||||
clone.push(new CameraPicture(texture, imageUrl));
|
||||
|
||||
setCameraRoll(clone);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
simpleAlert(LocalizeText('camera.error.body'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsCapturing(false);
|
||||
}
|
||||
|
||||
PlaySound(SoundNames.CAMERA_SHUTTER);
|
||||
clone.push(new CameraPicture(texture, TextureUtils.generateImageUrl(texture)));
|
||||
|
||||
setCameraRoll(clone);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -62,7 +80,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
<Column center className="nitro-camera-capture" gap={ 0 }>
|
||||
{ selectedPicture && <img alt="" className="camera-area" src={ selectedPicture.imageUrl } /> }
|
||||
<div className="camera-canvas drag-handler">
|
||||
<div className="position-absolute info-camera" onClick={ () => CreateLinkEvent('habbopages/camera') }></div>
|
||||
<div className="position-absolute info-camera" onClick={ () => CreateLinkEvent('habbopages/camera') }></div>
|
||||
<div className="position-absolute header-close" onClick={ onClose }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</div>
|
||||
@ -88,4 +106,4 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
|
||||
</Column>
|
||||
</DraggableWindowCamera>
|
||||
);
|
||||
}
|
||||
}
|
@ -46,15 +46,17 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
|
||||
useMessageEvent<CameraStorageUrlMessageEvent>(CameraStorageUrlMessageEvent, event =>
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if (!parser) return null;
|
||||
if (!parser) return;
|
||||
|
||||
if (parser.notEnoughCredits && !parser.notEnoughActivityPoints) simpleAlert(LocalizeText('catalog.alert.notenough.credits.description'), null, null, null, LocalizeText('catalog.alert.notenough.title'));
|
||||
|
||||
@ -91,7 +93,6 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!base64Url) return;
|
||||
|
||||
GetRoomEngine().saveBase64AsScreenshot(base64Url);
|
||||
}, [ base64Url ]);
|
||||
|
||||
@ -102,12 +103,15 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
|
||||
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
|
||||
<NitroCardContentView>
|
||||
<Flex center>
|
||||
{ (pictureUrl && pictureUrl.length) &&
|
||||
<LayoutImage className="picture-preview border" imageUrl={ pictureUrl } /> }
|
||||
{ (!pictureUrl || !pictureUrl.length) &&
|
||||
{ (pictureUrl && pictureUrl.length) ? (
|
||||
<LayoutImage className="picture-preview border" imageUrl={ pictureUrl } />
|
||||
) : base64Url ? (
|
||||
<LayoutImage className="picture-preview border" imageUrl={ base64Url } />
|
||||
) : (
|
||||
<Flex center className="picture-preview border">
|
||||
<Text bold>{ LocalizeText('camera.loading') }</Text>
|
||||
</Flex> }
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
|
||||
<Column size={ publishDisabled ? 10 : 6 } gap={ 1 }>
|
||||
@ -172,4 +176,4 @@ export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
}
|
||||
}
|
@ -36,26 +36,31 @@ 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);
|
||||
if (!roomObject) return;
|
||||
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
|
||||
if (!roomObject) return '';
|
||||
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;
|
||||
|
||||
const imageUrl = currentImage.w || '';
|
||||
|
||||
return (
|
||||
<Grid style={ { display: 'flex', flexDirection: 'column' } }>
|
||||
<Flex center className="picture-preview border border-black" style={ currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {} }>
|
||||
{ !currentImage.w && <Text bold>{ LocalizeText('camera.loading') }</Text> }
|
||||
<Flex center className="picture-preview border border-black" style={ imageUrl ? { backgroundImage: `url(${imageUrl})` } : {} }>
|
||||
{ !imageUrl && <Text bold>{ LocalizeText('camera.loading') }</Text> }
|
||||
</Flex>
|
||||
{ currentImage.m && currentImage.m.length && <Text center>{ currentImage.m }</Text> }
|
||||
<Flex alignItems="center" justifyContent="between">
|
||||
<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>
|
||||
{ (currentPhotos.length > 1) &&
|
||||
<Flex className="picture-preview-buttons">
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect, TextureUtils, NitroSprite } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, GetRoomCameraWidgetManager, LocalizeText } from '../../../../api';
|
||||
import { Button, ButtonGroup, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
|
||||
import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView';
|
||||
import { ColorMatrixFilter } from '@pixi/filter-color-matrix';
|
||||
|
||||
export interface CameraWidgetEditorViewProps
|
||||
{
|
||||
@ -26,6 +27,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
|
||||
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
|
||||
const [ isZoomed, setIsZoomed ] = useState(false);
|
||||
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string | null>(null);
|
||||
|
||||
const getColorMatrixEffects = useMemo(() =>
|
||||
{
|
||||
@ -52,12 +54,12 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
|
||||
|
||||
return selectedEffects.findIndex(effect => (effect.effect.name === name));
|
||||
}, [ selectedEffects ])
|
||||
}, [ selectedEffects ]);
|
||||
|
||||
const getCurrentEffectIndex = useMemo(() =>
|
||||
{
|
||||
return getSelectedEffectIndex(selectedEffectName)
|
||||
}, [ selectedEffectName, getSelectedEffectIndex ])
|
||||
return getSelectedEffectIndex(selectedEffectName);
|
||||
}, [ selectedEffectName, getSelectedEffectIndex ]);
|
||||
|
||||
const getCurrentEffect = useMemo(() =>
|
||||
{
|
||||
@ -83,9 +85,66 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
});
|
||||
}, [ 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 ]);
|
||||
|
||||
const processAction = useCallback((type: string, effectName: string = null) =>
|
||||
@ -99,7 +158,9 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
onCancel();
|
||||
return;
|
||||
case 'checkout':
|
||||
onCheckout(getCurrentPictureUrl);
|
||||
if (currentPictureUrl) {
|
||||
onCheckout(currentPictureUrl);
|
||||
}
|
||||
return;
|
||||
case 'change_tab':
|
||||
setCurrentTab(String(effectName));
|
||||
@ -145,7 +206,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
case 'download': {
|
||||
const image = new Image();
|
||||
|
||||
image.src = getCurrentPictureUrl
|
||||
image.src = currentPictureUrl || '';
|
||||
|
||||
const newWindow = window.open('');
|
||||
newWindow.document.write(image.outerHTML);
|
||||
@ -155,18 +216,32 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
setIsZoomed(!isZoomed);
|
||||
return;
|
||||
}
|
||||
}, [ isZoomed, availableEffects, selectedEffectName, getCurrentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]);
|
||||
}, [ isZoomed, availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const thumbnails: CameraPictureThumbnail[] = [];
|
||||
|
||||
for(const effect of availableEffects)
|
||||
{
|
||||
thumbnails.push(new CameraPictureThumbnail(effect.name, GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false).src));
|
||||
if (!picture || !picture.texture) {
|
||||
setEffectsThumbnails([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setEffectsThumbnails(thumbnails);
|
||||
const thumbnails: CameraPictureThumbnail[] = [];
|
||||
|
||||
const generateThumbnails = async () =>
|
||||
{
|
||||
for(const effect of availableEffects)
|
||||
{
|
||||
const thumbnailSrc = await applyEffectsWithFallback(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false);
|
||||
if (thumbnailSrc) {
|
||||
thumbnails.push(new CameraPictureThumbnail(effect.name, thumbnailSrc));
|
||||
}
|
||||
}
|
||||
setEffectsThumbnails(thumbnails);
|
||||
};
|
||||
|
||||
generateThumbnails().catch(error => {
|
||||
setEffectsThumbnails([]);
|
||||
});
|
||||
}, [ picture, availableEffects ]);
|
||||
|
||||
return (
|
||||
@ -185,7 +260,11 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
</Column>
|
||||
<Column size={ 7 } justifyContent="between" overflow="hidden">
|
||||
<Column center>
|
||||
<LayoutImage imageUrl={ getCurrentPictureUrl } className="picture-preview" />
|
||||
{ currentPictureUrl ? (
|
||||
<LayoutImage imageUrl={ currentPictureUrl } className="picture-preview" />
|
||||
) : (
|
||||
<Text center bold>{ LocalizeText('camera.loading.error') }</Text>
|
||||
) }
|
||||
{ selectedEffectName &&
|
||||
<Column center fullWidth gap={ 1 }>
|
||||
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
|
||||
@ -195,7 +274,11 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
step={ 0.01 }
|
||||
value={ getCurrentEffect.alpha }
|
||||
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>
|
||||
<Flex justifyContent="between">
|
||||
@ -203,7 +286,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
<Button onClick={ event => processAction('clear_effects') }>
|
||||
<FaTrash className="fa-icon" />
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('download') }>
|
||||
<Button onClick={ event => processAction('download') } disabled={ !currentPictureUrl }>
|
||||
<FaSave className="fa-icon" />
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('zoom') }>
|
||||
@ -215,7 +298,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
<Button onClick={ event => processAction('cancel') }>
|
||||
{ LocalizeText('generic.cancel') }
|
||||
</Button>
|
||||
<Button onClick={ event => processAction('checkout') }>
|
||||
<Button onClick={ event => processAction('checkout') } disabled={ !currentPictureUrl }>
|
||||
{ LocalizeText('camera.preview.button.text') }
|
||||
</Button>
|
||||
</Flex>
|
||||
@ -225,4 +308,4 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
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 }>
|
||||
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
|
||||
<LayoutAvatarImageView figure={ offer.product.extraParam } headOnly={ true } direction={ 3 } /> }
|
||||
<LayoutAvatarImageView figure={ offer.product.extraParam } direction={ 2 } /> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}
|
||||
|
@ -1,58 +1,43 @@
|
||||
import { MouseEventType } from "@nitrots/nitro-renderer";
|
||||
import { FC, MouseEvent, PropsWithChildren, useState } from "react";
|
||||
import {
|
||||
attemptBotPlacement,
|
||||
IBotItem,
|
||||
UnseenItemCategory,
|
||||
} from "../../../../api";
|
||||
import { attemptBotPlacement, IBotItem, UnseenItemCategory, } from "../../../../api";
|
||||
import { LayoutAvatarImageView, LayoutGridItem } from "../../../../common";
|
||||
import { useInventoryBots, useInventoryUnseenTracker } from "../../../../hooks";
|
||||
|
||||
export const InventoryBotItemView: FC<
|
||||
PropsWithChildren<{ botItem: IBotItem }>
|
||||
> = (props) => {
|
||||
const { botItem = null, children = null, ...rest } = props;
|
||||
const [isMouseDown, setMouseDown] = useState(false);
|
||||
const { selectedBot = null, setSelectedBot = null } = useInventoryBots();
|
||||
const { isUnseen = null } = useInventoryUnseenTracker();
|
||||
const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id);
|
||||
export const InventoryBotItemView: FC<PropsWithChildren<{ botItem: IBotItem }>> = props =>
|
||||
{
|
||||
const { botItem = null, children = null, ...rest } = props;
|
||||
const [ isMouseDown, setMouseDown ] = useState(false);
|
||||
const { selectedBot = null, setSelectedBot = null } = useInventoryBots();
|
||||
const { isUnseen = null } = useInventoryUnseenTracker();
|
||||
const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id);
|
||||
|
||||
const onMouseEvent = (event: MouseEvent) => {
|
||||
switch (event.type) {
|
||||
case MouseEventType.MOUSE_DOWN:
|
||||
setSelectedBot(botItem);
|
||||
setMouseDown(true);
|
||||
return;
|
||||
case MouseEventType.MOUSE_UP:
|
||||
setMouseDown(false);
|
||||
return;
|
||||
case MouseEventType.ROLL_OUT:
|
||||
if (!isMouseDown || selectedBot !== botItem) return;
|
||||
const onMouseEvent = (event: MouseEvent) =>
|
||||
{
|
||||
switch(event.type)
|
||||
{
|
||||
case MouseEventType.MOUSE_DOWN:
|
||||
setSelectedBot(botItem);
|
||||
setMouseDown(true);
|
||||
return;
|
||||
case MouseEventType.MOUSE_UP:
|
||||
setMouseDown(false);
|
||||
return;
|
||||
case MouseEventType.ROLL_OUT:
|
||||
if(!isMouseDown || (selectedBot !== botItem)) return;
|
||||
|
||||
attemptBotPlacement(botItem);
|
||||
return;
|
||||
case "dblclick":
|
||||
attemptBotPlacement(botItem);
|
||||
return;
|
||||
}
|
||||
};
|
||||
attemptBotPlacement(botItem);
|
||||
return;
|
||||
case 'dblclick':
|
||||
attemptBotPlacement(botItem);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutGridItem
|
||||
itemActive={selectedBot === botItem}
|
||||
itemUnseen={unseen}
|
||||
onMouseDown={onMouseEvent}
|
||||
onMouseUp={onMouseEvent}
|
||||
onMouseOut={onMouseEvent}
|
||||
onDoubleClick={onMouseEvent}
|
||||
{...rest}
|
||||
>
|
||||
<LayoutAvatarImageView
|
||||
figure={botItem.botData.figure}
|
||||
direction={3}
|
||||
headOnly={true}
|
||||
/>
|
||||
{children}
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<LayoutGridItem itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent } onDoubleClick={ onMouseEvent } { ...rest }>
|
||||
<LayoutAvatarImageView figure={ botItem.botData.figure } direction={ 2 } />
|
||||
{ children }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
@ -77,8 +77,7 @@
|
||||
color: #fff;
|
||||
width: 400px;
|
||||
min-height: 400px;
|
||||
max-height: 500px;
|
||||
overflow-y: hidden;
|
||||
max-height: 400px;
|
||||
border: 2px solid #ffd700;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||
@ -117,7 +116,7 @@
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
height: calc(100% - 34px);
|
||||
overflow-y: hidden;
|
||||
overflow-y: auto; /* Changed to auto to allow scrolling */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
@ -125,12 +124,9 @@
|
||||
}
|
||||
|
||||
.grid {
|
||||
animation: scroll-credits 20s linear infinite;
|
||||
animation: scroll-credits 40s linear infinite;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@ -169,8 +165,8 @@
|
||||
background-repeat: no-repeat;
|
||||
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7));
|
||||
}
|
||||
|
||||
.nitro-logo-default {
|
||||
|
||||
.nitro-logo-default {
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
background-image: url("@/assets/images/notifications/nitro.png");
|
||||
@ -178,16 +174,16 @@
|
||||
margin: 0 auto 20px;
|
||||
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7));
|
||||
}
|
||||
|
||||
.credits-divider {
|
||||
|
||||
.credits-divider {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
background: #000;
|
||||
margin: 10px auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
||||
.spacer {
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
@ -196,18 +192,14 @@
|
||||
@keyframes scroll-credits {
|
||||
0% {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
transform: translateY(80%);
|
||||
opacity: 1;
|
||||
5% {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
90% {
|
||||
transform: translateY(-80%);
|
||||
opacity: 1;
|
||||
95% {
|
||||
transform: translateY(-110%);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
transform: translateY(-150%);
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
|
||||
<Text bold fontSize={5}>Nitro React</Text>
|
||||
<Text>Nitro was created by billsonnn</Text>
|
||||
<div className="spacer"></div>
|
||||
<Text>Nitro Versions</Text>
|
||||
<Text>Nitro Versions</Text>
|
||||
<Text><b>Nitro:</b> {GetUIVersion()}</Text>
|
||||
</Column>
|
||||
<Column alignItems="center">
|
||||
@ -57,29 +57,44 @@ export const NitroSystemAlertView: FC<NotificationDefaultAlertViewProps> = props
|
||||
<Text>- DuckieTM (Re-Design)</Text>
|
||||
<Text>- Jonas (Contributing)</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')}>
|
||||
Cool UI Git
|
||||
</Button>
|
||||
</Column>
|
||||
</Flex>
|
||||
</Column>
|
||||
|
||||
<Column size={12}>
|
||||
<Column size={12}>
|
||||
<div className="credits-divider"></div>
|
||||
</Column>
|
||||
|
||||
<Column size={10}>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text center bold fontSize={5}>Special Thanks</Text>
|
||||
<Text>The whole Discord community !!</Text>
|
||||
<Text>- Billsonnn for creating Nitro.</Text>
|
||||
<Text>- Remco for testing.</Text>
|
||||
<Text>- Object from Atom.</Text>
|
||||
<Text>- Habbo for providing the assets</Text>
|
||||
</Column>
|
||||
</Column>
|
||||
<div className="notification-frank"></div>
|
||||
<Column size={10}>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text center bold fontSize={5}>Special Thanks</Text>
|
||||
<Text>The whole Discord community !!</Text>
|
||||
<Text>- Billsonnn for creating Nitro.</Text>
|
||||
<Text>- Remco for testing.</Text>
|
||||
<Text>- Object from Atom.</Text>
|
||||
<Text>- Habbo for providing the assets</Text>
|
||||
</Column>
|
||||
</Column>
|
||||
<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>
|
||||
</LayoutNotificationCredits>
|
||||
);
|
||||
|
@ -38,23 +38,24 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
const [ songId, setSongId ] = useState<number>(-1);
|
||||
const [ songName, setSongName ] = 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 =>
|
||||
{
|
||||
setSongId(event.id);
|
||||
}, (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);
|
||||
|
||||
if(!songInfo) return;
|
||||
if (!songInfo) return;
|
||||
|
||||
setSongName(songInfo.name);
|
||||
setSongCreator(songInfo.creator);
|
||||
setSongName(songInfo.name || '');
|
||||
setSongCreator(songInfo.creator || '');
|
||||
}, (isJukeBox || isSongDisk));
|
||||
|
||||
useEffect(() =>
|
||||
@ -75,95 +76,105 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
let furniIsJukebox = false;
|
||||
let furniIsSongDisk = false;
|
||||
let furniSongId = -1;
|
||||
|
||||
const roomObject = GetRoomEngine().getRoomObject( roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR );
|
||||
const location = roomObject.getLocation();
|
||||
if (location) {
|
||||
setItemLocation({ x: location.x, y: location.y, z: location.z, });
|
||||
}
|
||||
|
||||
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
|
||||
const location = roomObject.getLocation();
|
||||
if (location) {
|
||||
setItemLocation({ x: location.x, y: location.y, z: location.z });
|
||||
}
|
||||
|
||||
const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST);
|
||||
|
||||
if(isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)
|
||||
if (isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)
|
||||
{
|
||||
canMove = true;
|
||||
canRotate = !avatarInfo.isWallItem;
|
||||
|
||||
if(avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true;
|
||||
if (avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true;
|
||||
}
|
||||
|
||||
if(avatarInfo.isAnyRoomController)
|
||||
if (avatarInfo.isAnyRoomController)
|
||||
{
|
||||
canSeeFurniId = true;
|
||||
}
|
||||
|
||||
if((((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY) || ((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT) && isValidController)) canUse = true;
|
||||
|
||||
if(avatarInfo.extraParam)
|
||||
{
|
||||
if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI)
|
||||
{
|
||||
const stuffData = (avatarInfo.stuffData as CrackableDataType);
|
||||
|
||||
try {
|
||||
if (
|
||||
avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY ||
|
||||
(avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER && isValidController) ||
|
||||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX && isValidController) ||
|
||||
(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT && isValidController)
|
||||
) {
|
||||
canUse = true;
|
||||
isCrackable = true;
|
||||
crackableHits = stuffData.hits;
|
||||
crackableTarget = stuffData.target;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error checking usage policy:', error);
|
||||
}
|
||||
|
||||
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
|
||||
{
|
||||
const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist();
|
||||
|
||||
if(playlist)
|
||||
if (avatarInfo.extraParam)
|
||||
{
|
||||
try {
|
||||
if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI)
|
||||
{
|
||||
furniSongId = playlist.nowPlayingSongId;
|
||||
const stuffData = (avatarInfo.stuffData as CrackableDataType);
|
||||
|
||||
canUse = true;
|
||||
isCrackable = true;
|
||||
crackableHits = stuffData.hits;
|
||||
crackableTarget = stuffData.target;
|
||||
}
|
||||
else if (avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
|
||||
{
|
||||
const playlist = GetNitroInstance().soundManager.musicController.getRoomItemPlaylist();
|
||||
if (playlist)
|
||||
{
|
||||
furniSongId = playlist.nowPlayingSongId;
|
||||
}
|
||||
furniIsJukebox = true;
|
||||
}
|
||||
else if (avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
|
||||
{
|
||||
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length)) || -1;
|
||||
furniIsSongDisk = true;
|
||||
}
|
||||
|
||||
furniIsJukebox = true;
|
||||
}
|
||||
|
||||
else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
|
||||
{
|
||||
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length));
|
||||
|
||||
furniIsSongDisk = true;
|
||||
}
|
||||
|
||||
if(godMode)
|
||||
{
|
||||
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
|
||||
|
||||
if(extraParam)
|
||||
if (godMode)
|
||||
{
|
||||
const parts = extraParam.split('\t');
|
||||
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
|
||||
|
||||
for(const part of parts)
|
||||
if (extraParam)
|
||||
{
|
||||
const value = part.split('=');
|
||||
const parts = extraParam.split('\t');
|
||||
|
||||
if(value && (value.length === 2))
|
||||
for (const part of parts)
|
||||
{
|
||||
furniKeyss.push(value[0]);
|
||||
furniValuess.push(value[1]);
|
||||
const value = part.split('=');
|
||||
|
||||
if (value && (value.length === 2))
|
||||
{
|
||||
furniKeyss.push(value[0]);
|
||||
furniValuess.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error processing extraParam:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if(godMode)
|
||||
if (godMode)
|
||||
{
|
||||
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, (avatarInfo.isWallItem) ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
|
||||
|
||||
if(roomObject)
|
||||
if (roomObject)
|
||||
{
|
||||
const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
|
||||
const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA);
|
||||
|
||||
if(customVariables && customVariables.length)
|
||||
if (customVariables && customVariables.length)
|
||||
{
|
||||
for(const customVariable of customVariables)
|
||||
for (const customVariable of customVariables)
|
||||
{
|
||||
customKeyss.push(customVariable);
|
||||
customValuess.push((furnitureData[customVariable]) || '');
|
||||
@ -172,11 +183,10 @@ export const InfoStandWidgetFurniView: FC<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);
|
||||
setCanMove(canMove);
|
||||
@ -196,16 +206,26 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
setIsSongDisk(furniIsSongDisk);
|
||||
setSongId(furniSongId);
|
||||
|
||||
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
|
||||
if (avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
|
||||
|
||||
if (avatarInfo.image instanceof Promise) {
|
||||
avatarInfo.image.then(image => {
|
||||
setFurniImage(image);
|
||||
}).catch(error => {
|
||||
setFurniImage(null);
|
||||
});
|
||||
} else {
|
||||
setFurniImage(avatarInfo.image);
|
||||
}
|
||||
}, [ roomSession, avatarInfo ]);
|
||||
|
||||
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return;
|
||||
if (!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return;
|
||||
|
||||
if(groupName) setGroupName(null);
|
||||
if (groupName) setGroupName(null);
|
||||
|
||||
setGroupName(parser.title);
|
||||
});
|
||||
@ -213,44 +233,36 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
useEffect(() =>
|
||||
{
|
||||
const songInfo = GetNitroInstance().soundManager.musicController.getSongInfo(songId);
|
||||
|
||||
setSongName(songInfo?.name ?? '');
|
||||
setSongCreator(songInfo?.creator ?? '');
|
||||
setSongName(songInfo && songInfo.name ? songInfo.name : '');
|
||||
setSongCreator(songInfo && songInfo.creator ? songInfo.creator : '');
|
||||
}, [ songId ]);
|
||||
|
||||
const onFurniSettingChange = useCallback((index: number, value: string) =>
|
||||
{
|
||||
const clone = Array.from(furniValues);
|
||||
|
||||
clone[index] = value;
|
||||
|
||||
setFurniValues(clone);
|
||||
}, [ furniValues ]);
|
||||
|
||||
const onCustomVariableChange = useCallback((index: number, value: string) =>
|
||||
{
|
||||
const clone = Array.from(customValues);
|
||||
|
||||
clone[index] = value;
|
||||
|
||||
setCustomValues(clone);
|
||||
}, [ customValues ]);
|
||||
|
||||
const getFurniSettingsAsString = useCallback(() =>
|
||||
{
|
||||
if(furniKeys.length === 0 || furniValues.length === 0) return '';
|
||||
if (furniKeys.length === 0 || furniValues.length === 0) return '';
|
||||
|
||||
let data = '';
|
||||
|
||||
let i = 0;
|
||||
|
||||
while(i < furniKeys.length)
|
||||
while (i < furniKeys.length)
|
||||
{
|
||||
const key = furniKeys[i];
|
||||
const value = furniValues[i];
|
||||
|
||||
data = (data + (key + '=' + value + '\t'));
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
@ -259,11 +271,11 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
|
||||
const processButtonAction = useCallback((action: string) =>
|
||||
{
|
||||
if(!action || (action === '')) return;
|
||||
if (!action || (action === '')) return;
|
||||
|
||||
let objectData: string = null;
|
||||
|
||||
switch(action)
|
||||
switch (action)
|
||||
{
|
||||
case 'buy_one':
|
||||
CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`);
|
||||
@ -275,7 +287,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
|
||||
break;
|
||||
case 'pickup':
|
||||
if(pickupMode === PICKUP_MODE_FULL)
|
||||
if (pickupMode === PICKUP_MODE_FULL)
|
||||
{
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP);
|
||||
}
|
||||
@ -291,12 +303,11 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
const mapData = new Map<string, string>();
|
||||
const dataParts = getFurniSettingsAsString().split('\t');
|
||||
|
||||
if(dataParts)
|
||||
if (dataParts)
|
||||
{
|
||||
for(const part of dataParts)
|
||||
for (const part of dataParts)
|
||||
{
|
||||
const [ key, value ] = part.split('=', 2);
|
||||
|
||||
mapData.set(key, value);
|
||||
}
|
||||
}
|
||||
@ -307,12 +318,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
case 'save_custom_variables': {
|
||||
const map = new Map();
|
||||
|
||||
for(let i = 0; i < customKeys.length; i++)
|
||||
for (let i = 0; i < customKeys.length; i++)
|
||||
{
|
||||
const key = customKeys[i];
|
||||
const value = customValues[i];
|
||||
|
||||
if((key && key.length) && (value && value.length)) map.set(key, value);
|
||||
if ((key && key.length) && (value && value.length)) map.set(key, value);
|
||||
}
|
||||
|
||||
SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map));
|
||||
@ -325,12 +336,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
{
|
||||
const stringDataType = (avatarInfo.stuffData as StringDataType);
|
||||
|
||||
if(!stringDataType || !(stringDataType instanceof StringDataType)) return null;
|
||||
if (!stringDataType || !(stringDataType instanceof StringDataType)) return null;
|
||||
|
||||
return stringDataType.getValue(2);
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
if(!avatarInfo) return null;
|
||||
if (!avatarInfo) return null;
|
||||
|
||||
return (
|
||||
<Column gap={ 1 } alignItems="end">
|
||||
@ -339,7 +350,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<Column gap={ 1 }>
|
||||
<Flex alignItems="center" justifyContent="between" gap={ 1 }>
|
||||
{ !(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 } />
|
||||
</Flex>
|
||||
<hr className="m-0" />
|
||||
@ -354,8 +365,12 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<div className="position-absolute end-0">
|
||||
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
|
||||
</div> }
|
||||
{ avatarInfo.image && avatarInfo.image.src.length &&
|
||||
<img className="d-block mx-auto" src={ avatarInfo.image.src } alt="" /> }
|
||||
{ furniImage && furniImage.src && typeof furniImage.src === 'string' && furniImage.src.length > 0 ?
|
||||
<>
|
||||
<img className="d-block mx-auto" src={ furniImage.src } alt="" />
|
||||
</> :
|
||||
console.log('Skipping furni image: Invalid or missing src', furniImage)
|
||||
}
|
||||
</Flex>
|
||||
<hr className="m-0" />
|
||||
</Column>
|
||||
@ -384,14 +399,14 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<Text variant="white" small wrap>
|
||||
{ LocalizeText('infostand.jukebox.text.not.playing') }
|
||||
</Text> }
|
||||
{ !!songName.length &&
|
||||
{ songName && songName.length > 0 &&
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Base className="icon disk-icon" />
|
||||
<Text variant="white" small wrap>
|
||||
{ songName }
|
||||
</Text>
|
||||
</Flex> }
|
||||
{ !!songCreator.length &&
|
||||
{ songCreator && songCreator.length > 0 &&
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Base className="icon disk-creator" />
|
||||
<Text variant="white" small wrap>
|
||||
@ -413,15 +428,15 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
|
||||
<Text variant="white" underline>{ groupName }</Text>
|
||||
</Flex>
|
||||
</> }
|
||||
<>
|
||||
<hr className="m-0" />
|
||||
<Text small wrap variant="white">
|
||||
X = {itemLocation.x} and Y = {itemLocation.y}<br />
|
||||
BuildHeight = {itemLocation.z < 0.01 ? 0 : itemLocation.z}<br />
|
||||
{ canSeeFurniId && <Text wrap variant="white"> Room Furnishing ID: { avatarInfo.id }</Text> }
|
||||
</Text>
|
||||
</>
|
||||
{itemLocation.x > -1}
|
||||
<>
|
||||
<hr className="m-0" />
|
||||
<Text small wrap variant="white">
|
||||
X = {itemLocation.x} and Y = {itemLocation.y}<br />
|
||||
BuildHeight = {itemLocation.z < 0.01 ? 0 : itemLocation.z}<br />
|
||||
{ canSeeFurniId && <Text wrap variant="white"> Room Furnishing ID: { avatarInfo.id }</Text> }
|
||||
</Text>
|
||||
</>
|
||||
{itemLocation.x > -1}
|
||||
{ godMode &&
|
||||
<>
|
||||
<hr className="m-0" />
|
||||
|
@ -157,7 +157,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
|
||||
<Flex gap={ 1 }>
|
||||
<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 }` }/>
|
||||
<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 }` }/>
|
||||
{ avatarInfo.type === AvatarInfoUser.OWN_USER &&
|
||||
<Base position="absolute" className="icon edit-icon edit-icon-position" onClick={ event =>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react';
|
||||
import { CreateLinkEvent, DispatchUiEvent, GetConfiguration, GetRoomEngine, GetRoomSession, GetUserProfile } from '../../api';
|
||||
import { CreateLinkEvent, DispatchUiEvent, GetConfiguration, GetRoomEngine, GetRoomSession, GetSessionDataManager, GetUserProfile } from '../../api';
|
||||
import { Base, Flex, LayoutItemCountView } from '../../common';
|
||||
import { GuideToolEvent } from '../../events';
|
||||
|
||||
|
@ -22,38 +22,39 @@
|
||||
},
|
||||
"main": "./index",
|
||||
"dependencies": {
|
||||
"@pixi/app": "~6.5.10",
|
||||
"@pixi/basis": "~6.5.10",
|
||||
"@pixi/canvas-display": "~6.5.10",
|
||||
"@pixi/canvas-extract": "~6.5.10",
|
||||
"@pixi/canvas-renderer": "~6.5.10",
|
||||
"@pixi/constants": "~6.5.10",
|
||||
"@pixi/core": "~6.5.10",
|
||||
"@pixi/display": "~6.5.10",
|
||||
"@pixi/events": "~6.5.10",
|
||||
"@pixi/extensions": "~6.5.10",
|
||||
"@pixi/extract": "~6.5.10",
|
||||
"@pixi/filter-alpha": "~6.5.10",
|
||||
"@pixi/filter-color-matrix": "~6.5.10",
|
||||
"@pixi/graphics": "~6.5.10",
|
||||
"@pixi/graphics-extras": "~6.5.10",
|
||||
"@pixi/app": "~7.4.3",
|
||||
"@pixi/assets": "~7.4.3",
|
||||
"@pixi/basis": "~7.4.3",
|
||||
"@pixi/canvas-display": "~7.4.3",
|
||||
"@pixi/canvas-extract": "~7.4.3",
|
||||
"@pixi/canvas-renderer": "~7.4.3",
|
||||
"@pixi/constants": "~7.4.3",
|
||||
"@pixi/core": "~7.4.3",
|
||||
"@pixi/display": "~7.4.3",
|
||||
"@pixi/events": "~7.4.3",
|
||||
"@pixi/extensions": "~7.4.3",
|
||||
"@pixi/extract": "~7.4.3",
|
||||
"@pixi/filter-alpha": "~7.4.3",
|
||||
"@pixi/filter-color-matrix": "~7.4.3",
|
||||
"@pixi/graphics": "~7.4.3",
|
||||
"@pixi/graphics-extras": "~7.4.3",
|
||||
"@pixi/interaction": "~6.5.10",
|
||||
"@pixi/loaders": "~6.5.10",
|
||||
"@pixi/math": "~6.5.10",
|
||||
"@pixi/math-extras": "~6.5.10",
|
||||
"@pixi/mixin-cache-as-bitmap": "~6.5.10",
|
||||
"@pixi/mixin-get-child-by-name": "~6.5.10",
|
||||
"@pixi/mixin-get-global-position": "~6.5.10",
|
||||
"@pixi/math": "~7.4.3",
|
||||
"@pixi/math-extras": "~7.4.3",
|
||||
"@pixi/mixin-cache-as-bitmap": "~7.4.3",
|
||||
"@pixi/mixin-get-child-by-name": "~7.4.3",
|
||||
"@pixi/mixin-get-global-position": "~7.4.3",
|
||||
"@pixi/polyfill": "~6.5.10",
|
||||
"@pixi/runner": "~6.5.10",
|
||||
"@pixi/settings": "~6.5.10",
|
||||
"@pixi/sprite": "~6.5.10",
|
||||
"@pixi/sprite-tiling": "~6.5.10",
|
||||
"@pixi/spritesheet": "~6.5.10",
|
||||
"@pixi/text": "~6.5.10",
|
||||
"@pixi/ticker": "~6.5.10",
|
||||
"@pixi/tilemap": "^3.2.2",
|
||||
"@pixi/utils": "~6.5.10",
|
||||
"@pixi/runner": "~7.4.3",
|
||||
"@pixi/settings": "~7.4.3",
|
||||
"@pixi/sprite": "~7.4.3",
|
||||
"@pixi/sprite-tiling": "~7.4.3",
|
||||
"@pixi/spritesheet": "~7.4.3",
|
||||
"@pixi/text": "~7.4.3",
|
||||
"@pixi/ticker": "~7.4.3",
|
||||
"@pixi/tilemap": "^5.0.1",
|
||||
"@pixi/utils": "~7.4.3",
|
||||
"clientjs": "^0.2.1",
|
||||
"gifuct-js": "^2.1.2",
|
||||
"howler": "^2.2.3",
|
||||
@ -68,7 +69,7 @@
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"eslint": "^8.20.0",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "~4.4.4",
|
||||
"typescript": "~5.4.5",
|
||||
"vite": "^4.0.2",
|
||||
"vite-plugin-minify": "^1.5.2"
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export interface IAvatarImage extends IDisposable
|
||||
getLayerData(_arg_1: ISpriteDataContainer): IAnimationLayerData;
|
||||
getImage(setType: string, hightlight: boolean, scale?: number, cache?: boolean): RenderTexture;
|
||||
getImageAsSprite(setType: string, scale?: number): Sprite;
|
||||
getCroppedImage(setType: string, scale?: number): HTMLImageElement;
|
||||
getCroppedImage(setType: string, scale?: number): Promise<HTMLImageElement>; // Ensure async
|
||||
getAsset(_arg_1: string): IGraphicAsset;
|
||||
getDirection(): number;
|
||||
getFigure(): IAvatarFigureContainer;
|
||||
@ -33,4 +33,4 @@ export interface IAvatarImage extends IDisposable
|
||||
animationHasResetOnToggle: boolean;
|
||||
resetAnimationFrameCounter(): void;
|
||||
mainAction: string;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export class NitroVersion
|
||||
{
|
||||
public static RENDERER_VERSION: string = '1.6.6';
|
||||
public static RENDERER_VERSION: string = '1.7.4';
|
||||
public static UI_VERSION: string = '';
|
||||
|
||||
public static sayHello(): void
|
||||
|
@ -1,6 +1,5 @@
|
||||
import '@pixi/canvas-display';
|
||||
import { BatchRenderer, extensions } from '@pixi/core';
|
||||
import { Extract } from '@pixi/extract';
|
||||
import { extensions } from '@pixi/core';
|
||||
import '@pixi/graphics-extras';
|
||||
import { AppLoaderPlugin } from '@pixi/loaders';
|
||||
import '@pixi/math-extras';
|
||||
@ -9,13 +8,8 @@ import '@pixi/mixin-get-child-by-name';
|
||||
import '@pixi/mixin-get-global-position';
|
||||
import '@pixi/polyfill';
|
||||
import { TilingSpriteRenderer } from '@pixi/sprite-tiling';
|
||||
import { SpritesheetLoader } from '@pixi/spritesheet';
|
||||
import { TickerPlugin } from '@pixi/ticker';
|
||||
|
||||
extensions.add(
|
||||
BatchRenderer,
|
||||
Extract,
|
||||
TilingSpriteRenderer,
|
||||
SpritesheetLoader,
|
||||
AppLoaderPlugin,
|
||||
TickerPlugin);
|
||||
TilingSpriteRenderer
|
||||
);
|
@ -504,58 +504,60 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener
|
||||
return container;
|
||||
}
|
||||
|
||||
public getCroppedImage(setType: string, scale: number = 1): HTMLImageElement
|
||||
public async getCroppedImage(setType: string, scale: number = 1): Promise<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);
|
||||
|
||||
if(!avatarCanvas) return null;
|
||||
if (!avatarCanvas) {
|
||||
console.warn('getCroppedImage: No avatar canvas');
|
||||
return null;
|
||||
}
|
||||
|
||||
const setTypes = this.getBodyParts(setType, this._mainAction.definition.geometryType, this._mainDirection);
|
||||
const container = new NitroContainer();
|
||||
|
||||
let partCount = (setTypes.length - 1);
|
||||
let partCount = setTypes.length - 1;
|
||||
let partsAdded = 0;
|
||||
|
||||
while(partCount >= 0)
|
||||
while (partCount >= 0)
|
||||
{
|
||||
const set = setTypes[partCount];
|
||||
const part = this._cache.getImageContainer(set, this._frameCounter);
|
||||
|
||||
if(part)
|
||||
if (part)
|
||||
{
|
||||
const partCacheContainer = part.image;
|
||||
|
||||
if(!partCacheContainer)
|
||||
{
|
||||
container.destroy({
|
||||
children: true
|
||||
});
|
||||
|
||||
if (!partCacheContainer) {
|
||||
console.warn(`getCroppedImage: No image for part ${set}`);
|
||||
container.destroy({ children: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
const point = part.regPoint.clone();
|
||||
|
||||
if(point)
|
||||
if (point)
|
||||
{
|
||||
point.x += avatarCanvas.offset.x;
|
||||
point.y += avatarCanvas.offset.y;
|
||||
|
||||
point.x += avatarCanvas.regPoint.x;
|
||||
point.y += avatarCanvas.regPoint.y;
|
||||
|
||||
const partContainer = new NitroContainer();
|
||||
|
||||
partContainer.addChild(partCacheContainer);
|
||||
|
||||
if(partContainer)
|
||||
if (partContainer)
|
||||
{
|
||||
partContainer.position.set(point.x, point.y);
|
||||
|
||||
container.addChild(partContainer);
|
||||
partsAdded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -565,9 +567,22 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener
|
||||
|
||||
const texture = TextureUtils.generateTexture(container, new Rectangle(0, 0, avatarCanvas.width, avatarCanvas.height));
|
||||
|
||||
const image = TextureUtils.generateImage(texture);
|
||||
if (!texture) {
|
||||
console.warn('getCroppedImage: Failed to generate texture');
|
||||
container.destroy({ children: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!image) return null;
|
||||
const image = await TextureUtils.generateImage(texture);
|
||||
|
||||
container.destroy({ children: true });
|
||||
|
||||
if (!image || !image.src) {
|
||||
console.warn('getCroppedImage: Invalid image generated', image);
|
||||
const fallback = new Image();
|
||||
fallback.src = '';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
@ -1058,4 +1073,4 @@ export class AvatarImage implements IAvatarImage, IAvatarEffectListener
|
||||
if(this._effectListener) this._effectListener.resetEffect(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,11 @@ export class RenderRoomMessageComposer implements IMessageComposer<ConstructorPa
|
||||
{
|
||||
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 binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
|
||||
@ -35,9 +39,15 @@ export class RenderRoomMessageComposer implements IMessageComposer<ConstructorPa
|
||||
|
||||
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 binaryData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
|
||||
|
||||
this._data.push(binaryData.byteLength, binaryData.buffer);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ export class FurnitureChangeStateWhenStepOnLogic extends FurnitureLogic
|
||||
let sizeX = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_SIZE_X);
|
||||
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];
|
||||
|
||||
|
@ -14,37 +14,51 @@ export class FurnitureDynamicThumbnailVisualization extends IsometricImageFurniV
|
||||
this._hasOutline = true;
|
||||
}
|
||||
|
||||
protected updateModel(scale: number): boolean {
|
||||
if (this.object) {
|
||||
const thumbnailUrl = this.getThumbnailURL();
|
||||
protected async updateModel(scale: number): Promise<boolean>
|
||||
{
|
||||
if (this.object)
|
||||
{
|
||||
const thumbnailUrl = this.getThumbnailURL();
|
||||
|
||||
if (this._cachedUrl !== thumbnailUrl) {
|
||||
this._cachedUrl = thumbnailUrl;
|
||||
if (this._cachedUrl !== thumbnailUrl)
|
||||
{
|
||||
this._cachedUrl = thumbnailUrl;
|
||||
|
||||
if (this._cachedUrl && this._cachedUrl !== '') {
|
||||
const image = new Image();
|
||||
if (this._cachedUrl && this._cachedUrl !== '')
|
||||
{
|
||||
const image = new Image();
|
||||
|
||||
image.src = thumbnailUrl;
|
||||
image.crossOrigin = '*';
|
||||
image.src = thumbnailUrl;
|
||||
image.crossOrigin = '*';
|
||||
|
||||
image.onload = () => {
|
||||
const texture = Texture.from(image);
|
||||
await new Promise<void>((resolve) => {
|
||||
image.onload = () => {
|
||||
const texture = Texture.from(image);
|
||||
|
||||
texture.baseTexture.scaleMode = SCALE_MODES.LINEAR;
|
||||
texture.baseTexture.scaleMode = SCALE_MODES.LINEAR;
|
||||
|
||||
this.setThumbnailImages(texture);
|
||||
};
|
||||
} else {
|
||||
this.setThumbnailImages(null);
|
||||
this.setThumbnailImages(texture);
|
||||
resolve();
|
||||
};
|
||||
image.onerror = () => {
|
||||
console.warn('FurnitureDynamicThumbnailVisualization: Failed to load thumbnail image', { thumbnailUrl });
|
||||
this.setThumbnailImages(null);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.setThumbnailImages(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.updateModel(scale);
|
||||
}
|
||||
return await super.updateModel(scale);
|
||||
}
|
||||
|
||||
protected getThumbnailURL(): string
|
||||
{
|
||||
throw (new Error('This method must be overridden!'));
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
|
||||
private _thumbnailImageNormal: Texture<Resource>;
|
||||
private _thumbnailDirection: number;
|
||||
private _thumbnailChanged: boolean;
|
||||
private _uniqueId: string;
|
||||
private _photoUrl: string;
|
||||
protected _hasOutline: boolean;
|
||||
|
||||
constructor()
|
||||
@ -22,7 +24,9 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
|
||||
this._thumbnailImageNormal = null;
|
||||
this._thumbnailDirection = -1;
|
||||
this._thumbnailChanged = false;
|
||||
this._hasOutline = false;
|
||||
this._uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
this._photoUrl = null;
|
||||
this._hasOutline = true;
|
||||
}
|
||||
|
||||
public get hasThumbnailImage(): boolean
|
||||
@ -30,58 +34,83 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
|
||||
return !(this._thumbnailImageNormal == null);
|
||||
}
|
||||
|
||||
public setThumbnailImages(texture: Texture<Resource>): void
|
||||
public setThumbnailImages(k: Texture<Resource>, url?: string): void
|
||||
{
|
||||
this._thumbnailImageNormal = texture;
|
||||
this._thumbnailImageNormal = k;
|
||||
this._photoUrl = url || null;
|
||||
this._thumbnailChanged = true;
|
||||
}
|
||||
|
||||
protected updateModel(scale: number): boolean
|
||||
public getPhotoUrl(): string
|
||||
{
|
||||
const flag = super.updateModel(scale);
|
||||
return this._photoUrl;
|
||||
}
|
||||
|
||||
if(!this._thumbnailChanged && (this._thumbnailDirection === this.direction)) return flag;
|
||||
protected async updateModel(scale: number): Promise<boolean>
|
||||
{
|
||||
const flag = await super.updateModel(scale);
|
||||
|
||||
this.refreshThumbnail();
|
||||
if (!this._thumbnailChanged && (this._thumbnailDirection === this.direction)) {
|
||||
return flag;
|
||||
}
|
||||
|
||||
await this.refreshThumbnail();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private refreshThumbnail(): void
|
||||
private async refreshThumbnail(): Promise<void>
|
||||
{
|
||||
if(this.asset == null) return;
|
||||
|
||||
if(this._thumbnailImageNormal)
|
||||
{
|
||||
this.addThumbnailAsset(this._thumbnailImageNormal, 64);
|
||||
if (this.asset == null) {
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.asset.disposeAsset(this.getThumbnailAssetName(64));
|
||||
|
||||
const thumbnailAssetName = this.getThumbnailAssetName(64);
|
||||
|
||||
if (this._thumbnailImageNormal) {
|
||||
await this.addThumbnailAsset(this._thumbnailImageNormal, 64);
|
||||
} else {
|
||||
const layerId = 2;
|
||||
const sprite = this.getSprite(layerId);
|
||||
}
|
||||
|
||||
this._thumbnailChanged = false;
|
||||
this._thumbnailDirection = this.direction;
|
||||
}
|
||||
|
||||
private addThumbnailAsset(texture: Texture<Resource>, scale: number): void
|
||||
private async addThumbnailAsset(k: Texture<Resource>, scale: number): Promise<void>
|
||||
{
|
||||
let layerId = 0;
|
||||
|
||||
while(layerId < this.totalSprites)
|
||||
while (layerId < this.totalSprites)
|
||||
{
|
||||
if(this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL)
|
||||
const layerTag = this.getLayerTag(scale, this.direction, layerId);
|
||||
|
||||
if (layerTag === IsometricImageFurniVisualization.THUMBNAIL)
|
||||
{
|
||||
const assetName = (this.cacheSpriteAssetName(scale, layerId, false) + this.getFrameNumber(scale, layerId));
|
||||
const asset = this.getAsset(assetName, layerId);
|
||||
const thumbnailAssetName = `${this.getThumbnailAssetName(scale)}-${this._uniqueId}`;
|
||||
const transformedTexture = await this.generateTransformedThumbnail(k, asset || { width: 64, height: 64, offsetX: 0, offsetY: 0 });
|
||||
|
||||
if(asset)
|
||||
{
|
||||
const _local_6 = this.generateTransformedThumbnail(texture, asset);
|
||||
const _local_7 = this.getThumbnailAssetName(scale);
|
||||
if (!transformedTexture) {
|
||||
console.warn('IsometricImageFurniVisualization: Failed to generate transformed thumbnail for asset', { assetName });
|
||||
return;
|
||||
}
|
||||
|
||||
this.asset.disposeAsset(_local_7);
|
||||
this.asset.addAsset(_local_7, _local_6, true, asset.offsetX, asset.offsetY, false, false);
|
||||
const baseOffsetX = asset?.offsetX || 0;
|
||||
const baseOffsetY = asset?.offsetY || 0;
|
||||
|
||||
const offsetX = baseOffsetX - (transformedTexture.width / 2);
|
||||
const offsetY = baseOffsetY - (transformedTexture.height / 2);
|
||||
|
||||
this.asset.addAsset(thumbnailAssetName, transformedTexture, true, offsetX, offsetY, false, false);
|
||||
|
||||
const sprite = this.getSprite(layerId);
|
||||
if (sprite) {
|
||||
sprite.texture = transformedTexture;
|
||||
} else {
|
||||
console.warn('IsometricImageFurniVisualization: Sprite not found for layer', { layerId });
|
||||
}
|
||||
|
||||
return;
|
||||
@ -91,71 +120,79 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza
|
||||
}
|
||||
}
|
||||
|
||||
protected generateTransformedThumbnail(texture: Texture<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);
|
||||
const sprite = new NitroSprite(texture);
|
||||
|
||||
background.tint = 0x000000;
|
||||
background.width = (texture.width + 40);
|
||||
background.height = (texture.height + 40);
|
||||
|
||||
const sprite = new NitroSprite(texture);
|
||||
const offsetX = ((background.width - sprite.width) / 2);
|
||||
const offsetY = ((background.height - sprite.height) / 2);
|
||||
|
||||
sprite.position.set(offsetX, offsetY);
|
||||
|
||||
container.addChild(background, sprite);
|
||||
|
||||
texture = TextureUtils.generateTexture(container);
|
||||
}
|
||||
|
||||
texture.orig.width = asset.width;
|
||||
texture.orig.height = asset.height;
|
||||
const photoContainer = new NitroSprite();
|
||||
sprite.position.set(0, 0);
|
||||
photoContainer.addChild(sprite);
|
||||
|
||||
const scaleFactor = (asset?.width || 64) / texture.width;
|
||||
const matrix = new Matrix();
|
||||
|
||||
switch(this.direction)
|
||||
{
|
||||
switch (this.direction) {
|
||||
case 2:
|
||||
matrix.b = -(0.5);
|
||||
matrix.d /= 1.6;
|
||||
matrix.ty = ((0.5) * texture.width);
|
||||
matrix.a = scaleFactor;
|
||||
matrix.b = (-0.5 * scaleFactor);
|
||||
matrix.c = 0;
|
||||
matrix.d = scaleFactor;
|
||||
matrix.tx = 0;
|
||||
matrix.ty = (0.5 * scaleFactor * texture.width);
|
||||
break;
|
||||
case 0:
|
||||
case 4:
|
||||
matrix.b = (0.5);
|
||||
matrix.d /= 1.6;
|
||||
matrix.tx = -0.5;
|
||||
matrix.a = scaleFactor;
|
||||
matrix.b = (0.5 * scaleFactor);
|
||||
matrix.c = 0;
|
||||
matrix.d = scaleFactor;
|
||||
matrix.tx = 0;
|
||||
matrix.ty = 0;
|
||||
break;
|
||||
default:
|
||||
matrix.a = scaleFactor;
|
||||
matrix.b = 0;
|
||||
matrix.c = 0;
|
||||
matrix.d = scaleFactor;
|
||||
matrix.tx = 0;
|
||||
matrix.ty = 0;
|
||||
}
|
||||
|
||||
const sprite = new NitroSprite(texture);
|
||||
photoContainer.transform.setFromMatrix(matrix);
|
||||
|
||||
sprite.transform.setFromMatrix(matrix);
|
||||
const width = 64;
|
||||
const height = 64;
|
||||
|
||||
return TextureUtils.generateTexture(sprite);
|
||||
const container = new NitroSprite();
|
||||
|
||||
photoContainer.position.set(width / 2, height / 2);
|
||||
container.addChild(photoContainer);
|
||||
|
||||
const renderTexture = await TextureUtils.generateTexture(container, null, null, 1);
|
||||
if (!renderTexture) {
|
||||
console.warn('IsometricImageFurniVisualization: Failed to generate render texture for thumbnail');
|
||||
return texture;
|
||||
}
|
||||
|
||||
return renderTexture;
|
||||
}
|
||||
|
||||
protected getSpriteAssetName(scale: number, layerId: number): string
|
||||
{
|
||||
if(this._thumbnailImageNormal && (this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL)) return this.getThumbnailAssetName(scale);
|
||||
if (this._thumbnailImageNormal && (this.getLayerTag(scale, this.direction, layerId) === IsometricImageFurniVisualization.THUMBNAIL)) {
|
||||
return `${this.getThumbnailAssetName(scale)}-${this._uniqueId}`;
|
||||
}
|
||||
|
||||
return super.getSpriteAssetName(scale, layerId);
|
||||
}
|
||||
|
||||
protected getThumbnailAssetName(scale: number): string
|
||||
{
|
||||
this._thumbnailAssetNameNormal = this.getFullThumbnailAssetName(this.object.id, 64);
|
||||
|
||||
return this._thumbnailAssetNameNormal;
|
||||
return this.cacheSpriteAssetName(scale, 2, false) + this.getFrameNumber(scale, 2);
|
||||
}
|
||||
|
||||
protected getFullThumbnailAssetName(k: number, _arg_2: number): string
|
||||
{
|
||||
return [this._type, k, 'thumb', _arg_2].join('_');
|
||||
}
|
||||
}
|
||||
}
|
@ -12,31 +12,42 @@ export class PlaneTextureCache
|
||||
public RENDER_TEXTURE_POOL: Map<string, RenderTexture> = new Map();
|
||||
public RENDER_TEXTURE_CACHE: RenderTexture[] = [];
|
||||
|
||||
// Store an Extract instance
|
||||
private _extract: Extract;
|
||||
|
||||
constructor()
|
||||
{
|
||||
// Initialize Extract with the renderer
|
||||
const renderer = this.getRenderer();
|
||||
if (renderer) {
|
||||
this._extract = new Extract(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
public clearCache(): void
|
||||
{
|
||||
this.RENDER_TEXTURE_POOL.forEach(renderTexture => renderTexture?.destroy(true));
|
||||
|
||||
this.RENDER_TEXTURE_POOL.clear();
|
||||
this.RENDER_TEXTURE_CACHE = [];
|
||||
}
|
||||
|
||||
public clearRenderTexture(renderTexture: RenderTexture): RenderTexture
|
||||
{
|
||||
if(!renderTexture) return null;
|
||||
if (!renderTexture) return null;
|
||||
|
||||
return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture);
|
||||
}
|
||||
|
||||
private getTextureIdentifier(width: number, height: number, planeId: string): string
|
||||
{
|
||||
return `${ planeId ?? PlaneTextureCache.DEFAULT_PLANE_ID }:${ width }:${ height }`;
|
||||
return `${planeId ?? PlaneTextureCache.DEFAULT_PLANE_ID}:${width}:${height}`;
|
||||
}
|
||||
|
||||
public createRenderTexture(width: number, height: number, planeId: string = null): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
if(!planeId)
|
||||
if (!planeId)
|
||||
{
|
||||
const renderTexture = RenderTexture.create({
|
||||
width,
|
||||
@ -52,7 +63,7 @@ export class PlaneTextureCache
|
||||
|
||||
let renderTexture = this.RENDER_TEXTURE_POOL.get(planeId);
|
||||
|
||||
if(!renderTexture)
|
||||
if (!renderTexture)
|
||||
{
|
||||
renderTexture = RenderTexture.create({
|
||||
width,
|
||||
@ -60,7 +71,6 @@ export class PlaneTextureCache
|
||||
});
|
||||
|
||||
this.RENDER_TEXTURE_CACHE.push(renderTexture);
|
||||
|
||||
this.RENDER_TEXTURE_POOL.set(planeId, renderTexture);
|
||||
}
|
||||
|
||||
@ -69,7 +79,7 @@ export class PlaneTextureCache
|
||||
|
||||
public createAndFillRenderTexture(width: number, height: number, planeId = null, color: number = 16777215): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
const renderTexture = this.createRenderTexture(width, height, planeId);
|
||||
|
||||
@ -78,7 +88,7 @@ export class PlaneTextureCache
|
||||
|
||||
public createAndWriteRenderTexture(width: number, height: number, displayObject: DisplayObject, planeId: string = null, transform: Matrix = null): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
const renderTexture = this.createRenderTexture(width, height, planeId);
|
||||
|
||||
@ -87,12 +97,11 @@ export class PlaneTextureCache
|
||||
|
||||
public clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture
|
||||
{
|
||||
if(!renderTexture) return null;
|
||||
if (!renderTexture) return null;
|
||||
|
||||
const sprite = new Sprite(Texture.WHITE);
|
||||
|
||||
sprite.tint = color;
|
||||
|
||||
sprite.width = renderTexture.width;
|
||||
sprite.height = renderTexture.height;
|
||||
|
||||
@ -101,7 +110,7 @@ export class PlaneTextureCache
|
||||
|
||||
public writeToRenderTexture(displayObject: DisplayObject, renderTexture: RenderTexture, clear: boolean = true, transform: Matrix = null): RenderTexture
|
||||
{
|
||||
if(!displayObject || !renderTexture) return null;
|
||||
if (!displayObject || !renderTexture) return null;
|
||||
|
||||
this.getRenderer().render(displayObject, {
|
||||
renderTexture,
|
||||
@ -114,7 +123,7 @@ export class PlaneTextureCache
|
||||
|
||||
public getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array
|
||||
{
|
||||
return this.getExtractor().pixels(displayObject);
|
||||
return this.getExtractor().pixels(displayObject, frame);
|
||||
}
|
||||
|
||||
public getRenderer(): Renderer | AbstractRenderer
|
||||
@ -124,6 +133,10 @@ export class PlaneTextureCache
|
||||
|
||||
public getExtractor(): Extract
|
||||
{
|
||||
return (this.getRenderer().plugins.extract as Extract);
|
||||
// Return the stored Extract instance
|
||||
if (!this._extract) {
|
||||
throw new Error('Extract plugin not initialized. Ensure renderer is available.');
|
||||
}
|
||||
return this._extract;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,57 @@ import { PixiApplicationProxy } from './PixiApplicationProxy';
|
||||
|
||||
export class TextureUtils
|
||||
{
|
||||
private static _extract: Extract | null = null;
|
||||
|
||||
public static initialize(renderer: Renderer | AbstractRenderer): void
|
||||
{
|
||||
if (!this._extract && renderer) {
|
||||
this._extract = new Extract(renderer);
|
||||
console.log('TextureUtils: Initialized Extract plugin', { renderer });
|
||||
}
|
||||
}
|
||||
|
||||
public static async generateImage(target: DisplayObject | RenderTexture): Promise<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
|
||||
{
|
||||
if(!displayObject) return null;
|
||||
if (!displayObject) return null;
|
||||
|
||||
if(scaleMode === null) scaleMode = settings.SCALE_MODE;
|
||||
if (scaleMode === null) scaleMode = settings.SCALE_MODE;
|
||||
|
||||
return this.getRenderer().generateTexture(displayObject, {
|
||||
scaleMode,
|
||||
@ -23,42 +69,70 @@ export class TextureUtils
|
||||
|
||||
public static generateTextureFromImage(image: HTMLImageElement): Texture<Resource>
|
||||
{
|
||||
if(!image) return null;
|
||||
if (!image) return null;
|
||||
|
||||
return Texture.from(image);
|
||||
}
|
||||
|
||||
public static generateImage(target: DisplayObject | RenderTexture): HTMLImageElement
|
||||
public static async generateImageUrl(target: DisplayObject | RenderTexture): Promise<string>
|
||||
{
|
||||
if(!target) return null;
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getExtractor().image(target);
|
||||
}
|
||||
const extractor = this.getExtractor();
|
||||
if (!extractor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static generateImageUrl(target: DisplayObject | RenderTexture): string
|
||||
{
|
||||
if(!target) return null;
|
||||
let base64: string | Promise<string> = extractor.base64(target);
|
||||
|
||||
return this.getExtractor().base64(target);
|
||||
if (base64 && typeof base64 === 'object' && 'then' in base64) {
|
||||
try {
|
||||
base64 = await base64;
|
||||
} catch (error) {
|
||||
base64 = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) {
|
||||
const canvas = extractor.canvas(target);
|
||||
if (canvas) {
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
if (dataUrl && typeof dataUrl === 'string' && dataUrl.startsWith('data:image/')) {
|
||||
return dataUrl;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
|
||||
public static generateCanvas(target: DisplayObject | RenderTexture): HTMLCanvasElement
|
||||
{
|
||||
if(!target) return null;
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getExtractor().canvas(target);
|
||||
const extractor = this.getExtractor();
|
||||
if (!extractor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return extractor.canvas(target);
|
||||
}
|
||||
|
||||
public static clearRenderTexture(renderTexture: RenderTexture): RenderTexture
|
||||
{
|
||||
if(!renderTexture) return null;
|
||||
if (!renderTexture) return null;
|
||||
|
||||
return this.writeToRenderTexture(new Sprite(Texture.EMPTY), renderTexture);
|
||||
}
|
||||
|
||||
public static createRenderTexture(width: number, height: number): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
return RenderTexture.create({
|
||||
width,
|
||||
@ -68,7 +142,7 @@ export class TextureUtils
|
||||
|
||||
public static createAndFillRenderTexture(width: number, height: number, color: number = 16777215): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
const renderTexture = this.createRenderTexture(width, height);
|
||||
|
||||
@ -77,7 +151,7 @@ export class TextureUtils
|
||||
|
||||
public static createAndWriteRenderTexture(width: number, height: number, displayObject: DisplayObject, transform: Matrix = null): RenderTexture
|
||||
{
|
||||
if((width < 0) || (height < 0)) return null;
|
||||
if (width < 0 || height < 0) return null;
|
||||
|
||||
const renderTexture = this.createRenderTexture(width, height);
|
||||
|
||||
@ -86,12 +160,11 @@ export class TextureUtils
|
||||
|
||||
public static clearAndFillRenderTexture(renderTexture: RenderTexture, color: number = 16777215): RenderTexture
|
||||
{
|
||||
if(!renderTexture) return null;
|
||||
if (!renderTexture) return null;
|
||||
|
||||
const sprite = new Sprite(Texture.WHITE);
|
||||
|
||||
sprite.tint = color;
|
||||
|
||||
sprite.width = renderTexture.width;
|
||||
sprite.height = renderTexture.height;
|
||||
|
||||
@ -100,7 +173,7 @@ export class TextureUtils
|
||||
|
||||
public static writeToRenderTexture(displayObject: DisplayObject, renderTexture: RenderTexture, clear: boolean = true, transform: Matrix = null): RenderTexture
|
||||
{
|
||||
if(!displayObject || !renderTexture) return null;
|
||||
if (!displayObject || !renderTexture) return null;
|
||||
|
||||
this.getRenderer().render(displayObject, {
|
||||
renderTexture,
|
||||
@ -113,16 +186,33 @@ export class TextureUtils
|
||||
|
||||
public static getPixels(displayObject: DisplayObject | RenderTexture, frame: Rectangle = null): Uint8Array
|
||||
{
|
||||
return this.getExtractor().pixels(displayObject);
|
||||
const extractor = this.getExtractor();
|
||||
if (!extractor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return extractor.pixels(displayObject, frame);
|
||||
}
|
||||
|
||||
public static getRenderer(): Renderer | AbstractRenderer
|
||||
{
|
||||
return PixiApplicationProxy.instance.renderer;
|
||||
const renderer = PixiApplicationProxy.instance.renderer;
|
||||
if (!renderer) {
|
||||
console.warn('getRenderer: Renderer not available');
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public static getExtractor(): Extract
|
||||
{
|
||||
return (this.getRenderer().plugins.extract as Extract);
|
||||
if (!this._extract) {
|
||||
const renderer = this.getRenderer();
|
||||
if (renderer) {
|
||||
this._extract = new Extract(renderer);
|
||||
} else {
|
||||
throw new Error('Cannot initialize Extract: Renderer not available');
|
||||
}
|
||||
}
|
||||
return this._extract;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user