diff --git a/packages/api/src/nitro/room/object/RoomObjectVariable.ts b/packages/api/src/nitro/room/object/RoomObjectVariable.ts index 5c66dc1..e50525c 100644 --- a/packages/api/src/nitro/room/object/RoomObjectVariable.ts +++ b/packages/api/src/nitro/room/object/RoomObjectVariable.ts @@ -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'; diff --git a/packages/api/src/room/object/visualization/IRoomObjectSprite.ts b/packages/api/src/room/object/visualization/IRoomObjectSprite.ts index d5342d8..2066c17 100644 --- a/packages/api/src/room/object/visualization/IRoomObjectSprite.ts +++ b/packages/api/src/room/object/visualization/IRoomObjectSprite.ts @@ -11,6 +11,7 @@ export interface IRoomObjectSprite texture: Texture; width: number; height: number; + scale: number; offsetX: number; offsetY: number; flipH: boolean; diff --git a/packages/assets/src/AssetManager.ts b/packages/assets/src/AssetManager.ts index a9d652a..6e29525 100644 --- a/packages/assets/src/AssetManager.ts +++ b/packages/assets/src/AssetManager.ts @@ -200,7 +200,11 @@ export class AssetManager implements IAssetManager } else { - const texture = await Assets.load(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 + 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 + { + return new Promise((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 { const roomDataModule = await import('./assets/room/room.asset.json'); diff --git a/packages/room/src/object/logic/furniture/FurnitureRoomBrandingLogic.ts b/packages/room/src/object/logic/furniture/FurnitureRoomBrandingLogic.ts index cf935da..c6ee96a 100644 --- a/packages/room/src/object/logic/furniture/FurnitureRoomBrandingLogic.ts +++ b/packages/room/src/object/logic/furniture/FurnitureRoomBrandingLogic.ts @@ -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)); } @@ -147,10 +153,25 @@ export class FurnitureRoomBrandingLogic extends FurnitureLogic if(!texture) { - const status = await asset.downloadAsset(imageUrl); - - if(!status) + // 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); + + if(!status) + { + this.processUpdateMessage(new ObjectAdUpdateMessage(ObjectAdUpdateMessage.IMAGE_LOADING_FAILED)); + + return; + } + } + catch(error) + { + console.error(`[Soundboard/Branding] failed to load branding image "${ imageUrl }":`, error); + this.processUpdateMessage(new ObjectAdUpdateMessage(ObjectAdUpdateMessage.IMAGE_LOADING_FAILED)); return; diff --git a/packages/room/src/object/visualization/RoomObjectSprite.ts b/packages/room/src/object/visualization/RoomObjectSprite.ts index efeb808..4995b4d 100644 --- a/packages/room/src/object/visualization/RoomObjectSprite.ts +++ b/packages/room/src/object/visualization/RoomObjectSprite.ts @@ -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; diff --git a/packages/room/src/object/visualization/furniture/FurnitureBrandedImageVisualization.ts b/packages/room/src/object/visualization/furniture/FurnitureBrandedImageVisualization.ts index 5c312be..cde6c65 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureBrandedImageVisualization.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureBrandedImageVisualization.ts @@ -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(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_X) || 0); this._offsetY = (this.object.model.getValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Y) || 0); this._offsetZ = (this.object.model.getValue(RoomObjectVariable.FURNITURE_BRANDING_OFFSET_Z) || 0); + this._imageScale = (this.object.model.getValue(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; + } } diff --git a/packages/room/src/renderer/RoomSpriteCanvas.ts b/packages/room/src/renderer/RoomSpriteCanvas.ts index e3036bd..3480be7 100644 --- a/packages/room/src/renderer/RoomSpriteCanvas.ts +++ b/packages/room/src/renderer/RoomSpriteCanvas.ts @@ -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);