You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
feat: branding furni image position + scale (MPU background editor)
Renderer support for the in-client image position editor: - FurnitureBrandedImageVisualization applies offsetX/Y to the branded image layer only (offsetZ stays as z-index/depth), so the image can be moved without shifting the furni frame - new `scale` branding key + FURNITURE_BRANDING_SCALE: zooms the image via a real per-sprite scale (RoomObjectSprite.scale, default 1, applied in RoomSpriteCanvas) — NOT by writing the read-only width/height - AssetManager loads external raster images (png/jpg/…) via a CORS <img> + Texture.from instead of Assets.load (which didn't load cross-origin images); branding image download failures are now surfaced instead of swallowed
This commit is contained in:
@@ -76,6 +76,7 @@ export class RoomObjectVariable
|
||||
public static FURNITURE_BRANDING_OFFSET_X: string = 'furniture_branding_offset_x';
|
||||
public static FURNITURE_BRANDING_OFFSET_Y: string = 'furniture_branding_offset_y';
|
||||
public static FURNITURE_BRANDING_OFFSET_Z: string = 'furniture_branding_offset_z';
|
||||
public static FURNITURE_BRANDING_SCALE: string = 'furniture_branding_scale';
|
||||
public static FURNITURE_BADGE_IMAGE_STATUS: string = 'furniture_badge_image_status';
|
||||
public static FURNITURE_BADGE_ASSET_NAME: string = 'furniture_badge_asset_name';
|
||||
public static FURNITURE_BADGE_VISIBLE_IN_STATE: string = 'furniture_badge_visible_in_state';
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface IRoomObjectSprite
|
||||
texture: Texture;
|
||||
width: number;
|
||||
height: number;
|
||||
scale: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
flipH: boolean;
|
||||
|
||||
@@ -200,7 +200,11 @@ export class AssetManager implements IAssetManager
|
||||
}
|
||||
else
|
||||
{
|
||||
const texture = await Assets.load<Texture>(url);
|
||||
// External raster images (png/jpg/webp/…). Pixi's Assets.load does
|
||||
// not reliably load arbitrary cross-origin images in this setup, so
|
||||
// load them through a CORS-enabled <img> + Texture.from — the same
|
||||
// approach the badge / dynamic-thumbnail visualizations use.
|
||||
const texture = await this.loadExternalImageTexture(url);
|
||||
|
||||
if(texture) this.setTexture(url, texture);
|
||||
}
|
||||
@@ -213,6 +217,47 @@ export class AssetManager implements IAssetManager
|
||||
}
|
||||
}
|
||||
|
||||
private loadExternalImageTexture(url: string): Promise<Texture>
|
||||
{
|
||||
return new Promise<Texture>((resolve, reject) =>
|
||||
{
|
||||
const image = new Image();
|
||||
|
||||
// crossOrigin must be set BEFORE src so the request is made with CORS
|
||||
// and the resulting texture isn't tainted (WebGL would refuse it).
|
||||
image.crossOrigin = 'anonymous';
|
||||
|
||||
image.onload = () =>
|
||||
{
|
||||
image.onload = null;
|
||||
image.onerror = null;
|
||||
|
||||
if(image.complete && (image.width > 0) && (image.height > 0))
|
||||
{
|
||||
const texture = Texture.from(image);
|
||||
|
||||
if(texture.source) texture.source.scaleMode = 'linear';
|
||||
|
||||
resolve(texture);
|
||||
}
|
||||
else
|
||||
{
|
||||
reject(new Error(`image had no dimensions`));
|
||||
}
|
||||
};
|
||||
|
||||
image.onerror = () =>
|
||||
{
|
||||
image.onload = null;
|
||||
image.onerror = null;
|
||||
|
||||
reject(new Error(`image element failed to load (CORS / network / 404)`));
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
private async loadLocalRoom(): Promise<void>
|
||||
{
|
||||
const roomDataModule = await import('./assets/room/room.asset.json');
|
||||
|
||||
@@ -12,6 +12,7 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic
|
||||
public static OFFSETX_KEY: string = 'offsetX';
|
||||
public static OFFSETY_KEY: string = 'offsetY';
|
||||
public static OFFSETZ_KEY: string = 'offsetZ';
|
||||
public static SCALE_KEY: string = 'scale';
|
||||
|
||||
protected _disableFurnitureSelection: boolean;
|
||||
protected _hasClickUrl: boolean;
|
||||
@@ -93,6 +94,10 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic
|
||||
if(!isNaN(offsetY)) this.object.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Y, offsetY);
|
||||
if(!isNaN(offsetZ)) this.object.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Z, offsetZ);
|
||||
|
||||
const scaleRaw = parseInt(objectData.getValue(FurnitureRoomBrandingLogic.SCALE_KEY));
|
||||
const scale = isNaN(scaleRaw) ? 100 : scaleRaw;
|
||||
this.object.model.setValue(RoomObjectVariable.FURNITURE_BRANDING_SCALE, scale);
|
||||
|
||||
let options = (((FurnitureRoomBrandingLogic.IMAGEURL_KEY + '=') + ((imageUrl !== null) ? imageUrl : '')) + '\t');
|
||||
|
||||
if(this._hasClickUrl) options = (options + (((FurnitureRoomBrandingLogic.CLICKURL_KEY + '=') + ((clickUrl !== null) ? clickUrl : '')) + '\t'));
|
||||
@@ -100,6 +105,7 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic
|
||||
options = (options + (((FurnitureRoomBrandingLogic.OFFSETX_KEY + '=') + offsetX) + '\t'));
|
||||
options = (options + (((FurnitureRoomBrandingLogic.OFFSETY_KEY + '=') + offsetY) + '\t'));
|
||||
options = (options + (((FurnitureRoomBrandingLogic.OFFSETZ_KEY + '=') + offsetZ) + '\t'));
|
||||
options = (options + (((FurnitureRoomBrandingLogic.SCALE_KEY + '=') + scale) + '\t'));
|
||||
|
||||
this.object.model.setValue(RoomWidgetEnumItemExtradataParameter.INFOSTAND_EXTRA_PARAM, (RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS + options));
|
||||
}
|
||||
@@ -146,6 +152,12 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic
|
||||
const texture = asset.getTexture(imageUrl);
|
||||
|
||||
if(!texture)
|
||||
{
|
||||
// downloadAsset THROWS on failure (it doesn't return false), and this
|
||||
// method is fire-and-forget — so without this try/catch a failed image
|
||||
// download just vanishes and the furni keeps showing its default. Catch
|
||||
// it, surface the real reason, and flag the failed state.
|
||||
try
|
||||
{
|
||||
const status = await asset.downloadAsset(imageUrl);
|
||||
|
||||
@@ -156,6 +168,15 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.error(`[Soundboard/Branding] failed to load branding image "${ imageUrl }":`, error);
|
||||
|
||||
this.processUpdateMessage(new ObjectAdUpdateMessage(ObjectAdUpdateMessage.IMAGE_LOADING_FAILED));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.processUpdateMessage(new ObjectAdUpdateMessage(ObjectAdUpdateMessage.IMAGE_LOADED));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export class RoomObjectSprite implements IRoomObjectSprite
|
||||
|
||||
private _width: number = 0;
|
||||
private _height: number = 0;
|
||||
private _scale: number = 1;
|
||||
private _offsetX: number = 0;
|
||||
private _offsetY: number = 0;
|
||||
private _flipH: boolean = false;
|
||||
@@ -121,6 +122,21 @@ export class RoomObjectSprite implements IRoomObjectSprite
|
||||
return this._height;
|
||||
}
|
||||
|
||||
// Per-sprite zoom multiplier (1 = native). Applied on top of the room zoom.
|
||||
public get scale(): number
|
||||
{
|
||||
return this._scale;
|
||||
}
|
||||
|
||||
public set scale(value: number)
|
||||
{
|
||||
if(this._scale === value) return;
|
||||
|
||||
this._scale = value;
|
||||
|
||||
this._updateCounter++;
|
||||
}
|
||||
|
||||
public get offsetX(): number
|
||||
{
|
||||
return this._offsetX;
|
||||
|
||||
+48
@@ -18,6 +18,7 @@ export class FurnitureBrandedImageVisualization extends FurnitureVisualization
|
||||
protected _offsetX: number;
|
||||
protected _offsetY: number;
|
||||
protected _offsetZ: number;
|
||||
protected _imageScale: number;
|
||||
protected _currentFrame: number;
|
||||
protected _totalFrames: number;
|
||||
|
||||
@@ -32,6 +33,7 @@ export class FurnitureBrandedImageVisualization extends FurnitureVisualization
|
||||
this._offsetX = 0;
|
||||
this._offsetY = 0;
|
||||
this._offsetZ = 0;
|
||||
this._imageScale = 100;
|
||||
this._currentFrame = -1;
|
||||
this._totalFrames = -1;
|
||||
}
|
||||
@@ -65,6 +67,7 @@ export class FurnitureBrandedImageVisualization extends FurnitureVisualization
|
||||
this._offsetX = (this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_X) || 0);
|
||||
this._offsetY = (this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Y) || 0);
|
||||
this._offsetZ = (this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Z) || 0);
|
||||
this._imageScale = (this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BRANDING_SCALE) || 100);
|
||||
}
|
||||
|
||||
if(!this._imageReady)
|
||||
@@ -207,4 +210,49 @@ export class FurnitureBrandedImageVisualization extends FurnitureVisualization
|
||||
|
||||
return super.getSpriteAssetName(scale, layerId);
|
||||
}
|
||||
|
||||
private isBrandedImageLayer(scale: number, layerId: number): boolean
|
||||
{
|
||||
return this.getLayerTag(scale, this._direction, layerId) === FurnitureBrandedImageVisualization.BRANDED_IMAGE;
|
||||
}
|
||||
|
||||
// offsetX/Y move ONLY the branded image layer (the furni frame stays put).
|
||||
// Authored at full scale (64) and scaled with the room zoom.
|
||||
protected getLayerXOffset(scale: number, direction: number, layerId: number): number
|
||||
{
|
||||
const offset = super.getLayerXOffset(scale, direction, layerId);
|
||||
|
||||
return this.isBrandedImageLayer(scale, layerId) ? offset + ((this._offsetX || 0) * (scale / 64)) : offset;
|
||||
}
|
||||
|
||||
protected getLayerYOffset(scale: number, direction: number, layerId: number): number
|
||||
{
|
||||
const offset = super.getLayerYOffset(scale, direction, layerId);
|
||||
|
||||
return this.isBrandedImageLayer(scale, layerId) ? offset + ((this._offsetY || 0) * (scale / 64)) : offset;
|
||||
}
|
||||
|
||||
// offsetZ is the z-index / depth (overlap order, like CSS z-index).
|
||||
protected getLayerZOffset(scale: number, direction: number, layerId: number): number
|
||||
{
|
||||
const offset = super.getLayerZOffset(scale, direction, layerId);
|
||||
|
||||
return this.isBrandedImageLayer(scale, layerId) ? offset + (this._offsetZ || 0) : offset;
|
||||
}
|
||||
|
||||
// `scale` zooms the branded image (percent, 100 = 1x). Applied via the
|
||||
// sprite's scale (a real Pixi sprite scale) — NOT by writing width/height,
|
||||
// which are read-only on RoomObjectSprite and would throw every frame.
|
||||
protected updateSprite(scale: number, layerId: number): void
|
||||
{
|
||||
super.updateSprite(scale, layerId);
|
||||
|
||||
const sprite = this.getSprite(layerId);
|
||||
|
||||
if(!sprite) return;
|
||||
|
||||
sprite.scale = this.isBrandedImageLayer(scale, layerId)
|
||||
? Math.max(0.1, (this._imageScale || 100) / 100)
|
||||
: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,11 +702,13 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas
|
||||
if(extendedSprite.texture !== objectSprite.texture) extendedSprite.setTexture(objectSprite.texture);
|
||||
|
||||
|
||||
const sx = Math.abs(extendedSprite.scale.x) || 1;
|
||||
const sy = Math.abs(extendedSprite.scale.y) || 1;
|
||||
// Per-sprite zoom (objectSprite.scale, default 1) combined with flip.
|
||||
// Setting the magnitude directly (instead of reading the previous
|
||||
// scale) avoids compounding across frames.
|
||||
const magnitude = (objectSprite.scale && (objectSprite.scale > 0)) ? objectSprite.scale : 1;
|
||||
|
||||
extendedSprite.scale.x = objectSprite.flipH ? -sx : sx;
|
||||
extendedSprite.scale.y = objectSprite.flipV ? -sy : sy;
|
||||
extendedSprite.scale.x = objectSprite.flipH ? -magnitude : magnitude;
|
||||
extendedSprite.scale.y = objectSprite.flipV ? -magnitude : magnitude;
|
||||
}
|
||||
|
||||
extendedSprite.x = Math.round(sprite.x);
|
||||
|
||||
Reference in New Issue
Block a user