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:
medievalshell
2026-05-28 15:29:42 +02:00
parent 238592cd72
commit 9ece87240e
7 changed files with 142 additions and 8 deletions
+46 -1
View File
@@ -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');