From 120a502a7d4fde68112ff27b6be900cefa3ff905 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 5 Feb 2026 08:32:02 +0100 Subject: [PATCH] :up: Update - Animation canvas uses full landscape height for proper vertical positioning - Animation assets always loaded from room collection - Fallback to default plane when specific plane not found --- .../IAssetPlaneVisualizationLayer.ts | 1 + packages/assets/src/AssetManager.ts | 25 ++++- .../assets/src/assets/room/room.asset.json | 3 +- .../object/visualization/room/RoomPlane.ts | 92 ++++++++++++++++--- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/packages/api/src/asset/room-visualization/IAssetPlaneVisualizationLayer.ts b/packages/api/src/asset/room-visualization/IAssetPlaneVisualizationLayer.ts index e009893..bf0c2bc 100644 --- a/packages/api/src/asset/room-visualization/IAssetPlaneVisualizationLayer.ts +++ b/packages/api/src/asset/room-visualization/IAssetPlaneVisualizationLayer.ts @@ -4,4 +4,5 @@ export interface IAssetPlaneVisualizationLayer color?: number; offset?: number; align?: string; + backgroundColor?: string; } diff --git a/packages/assets/src/AssetManager.ts b/packages/assets/src/AssetManager.ts index ec53335..ca94dd5 100644 --- a/packages/assets/src/AssetManager.ts +++ b/packages/assets/src/AssetManager.ts @@ -99,6 +99,7 @@ export class AssetManager implements IAssetManager { if(!url || !url.length) return false; + // ✅ NEW: Local bundled assets (no generic.asset.url) if(url.startsWith('local://')) { const key = url.substring('local://'.length); @@ -172,32 +173,52 @@ export class AssetManager implements IAssetManager } } + /** + * ✅ Loads room assets from bundled code: + * - /packages/assets/src/assets/room/room.asset.json + * - /packages/assets/src/assets/room/images/*.png + * + * This loads individual PNG files instead of a spritesheet, then uses + * the JSON asset definitions for proper x, y offsets, flipH settings, etc. + */ private async loadLocalRoom(): Promise { + // 1) Load room JSON locally (bundled) const roomDataModule = await import('./assets/room/room.asset.json'); const roomData = (roomDataModule.default ?? roomDataModule) as IAssetData; + + // 2) Create collection WITHOUT spritesheet + // Note: Constructor calls define(), but assets won't be created since textures don't exist yet const collection = this.createCollection(roomData, null) as GraphicAssetCollection; if(!collection) return; + // 3) Load all images from the bundled assets const roomImages = import.meta.glob('./assets/room/*.png', { eager: true }); const roomImagesSub = import.meta.glob('./assets/room/images/*.png', { eager: true }); const merged = { ...roomImages, ...roomImagesSub }; + // 4) Register textures in the collection's _textures map + // getLibraryAsset() prepends collection name, so for asset 'wall_texture_64_0_wall_white' + // it looks for 'room_wall_texture_64_0_wall_white' in _textures for(const path in merged) { const mod = merged[path]; const imageUrl = (mod.default ?? mod) as string; const file = path.split('/').pop()!; - const rawName = file.replace(/\.png$/i, ''); + const rawName = file.replace(/\.png$/i, ''); // e.g., "room_wall_texture_64_0_wall_white" const texture = await Assets.load(imageUrl); if(!texture) continue; + // Register in AssetManager's global _textures for direct lookups this.setTexture(rawName, texture); + // Register in collection's _textures with the full name (room_...) + // This is what getLibraryAsset() will look for when defining assets collection.textures.set(rawName, texture); + // Also register without the "room_" prefix for direct lookups if(rawName.startsWith('room_')) { const normalizedName = rawName.substring('room_'.length); @@ -206,6 +227,8 @@ export class AssetManager implements IAssetManager } } + // 5) Now call define() again to process asset definitions with correct x, y offsets + // Assets that were skipped before (no textures) will now be created properly collection.define(roomData); } diff --git a/packages/assets/src/assets/room/room.asset.json b/packages/assets/src/assets/room/room.asset.json index 933112a..07e62a4 100644 --- a/packages/assets/src/assets/room/room.asset.json +++ b/packages/assets/src/assets/room/room.asset.json @@ -5729,7 +5729,8 @@ "allLayers": [ { "materialId": "landscape_64_background_1", - "align": "bottom" + "align": "bottom", + "backgroundColor": "#FEFEFE" }, { "items": [ diff --git a/packages/room/src/object/visualization/room/RoomPlane.ts b/packages/room/src/object/visualization/room/RoomPlane.ts index 2d9dfa3..f8a03eb 100644 --- a/packages/room/src/object/visualization/room/RoomPlane.ts +++ b/packages/room/src/object/visualization/room/RoomPlane.ts @@ -83,6 +83,7 @@ export class RoomPlane implements IRoomPlane private _landscapeForegroundTint: number = 0xffffff; private _landscapeBaseAlignBottom: boolean = false; private _landscapeForegroundAlignBottom: boolean = false; + private _landscapeBackgroundColor: number = null; private _hasWindowMask: boolean = false; private _windowMasks: { leftSideLoc: number; rightSideLoc: number }[] = []; @@ -256,6 +257,19 @@ export class RoomPlane implements IRoomPlane } } + // Fall back to "default" plane if the requested landscape plane wasn't found + if(!plane && planeType === RoomPlane.TYPE_LANDSCAPE) + { + const roomCollection2 = GetAssetManager().getCollection('room'); + const defaultPlaneData = roomCollection2?.data?.roomVisualization?.landscapeData; + plane = defaultPlaneData?.planes?.find(p => (p.id === 'default')); + if(plane) + { + planeVisualizationData = defaultPlaneData; + assetCollection = roomCollection2; + } + } + let planeVisualization = null; if(dataType === 'landscapeData') { @@ -280,6 +294,15 @@ export class RoomPlane implements IRoomPlane const baseAlignBottom = materialLayers[0]?.align === 'bottom'; const foregroundAlignBottom = materialLayers[1]?.align === 'bottom'; + // Parse backgroundColor from the first material layer (background layer) + const backgroundColorStr = materialLayers[0]?.backgroundColor; + let backgroundColor: number = null; + if(backgroundColorStr) + { + // Convert hex string like "#FEFEFE" to number + backgroundColor = parseInt(backgroundColorStr.replace('#', ''), 16); + } + const selectMaterialMatrixForNormal = (matrices = [], normal = null) => { if(!matrices.length) return null; @@ -351,20 +374,22 @@ export class RoomPlane implements IRoomPlane const foregroundTexture = resolveTextureForMaterial(foregroundMaterialId); const animationLayers: PlaneVisualizationAnimationLayer[] = []; - if(planeType === RoomPlane.TYPE_LANDSCAPE && planeVisualization?.allLayers && assetCollection) + if(planeType === RoomPlane.TYPE_LANDSCAPE && planeVisualization?.allLayers) { + // Always use the room collection for animation assets (clouds etc.) since they are stored there + const animationAssetCollection = roomCollection; for(const layer of planeVisualization.allLayers) { const animatedLayer = layer as IAssetPlaneVisualizationAnimatedLayer; if(animatedLayer?.items && animatedLayer.items.length > 0) { - const animLayer = new PlaneVisualizationAnimationLayer(animatedLayer.items, assetCollection); + const animLayer = new PlaneVisualizationAnimationLayer(animatedLayer.items, animationAssetCollection); if(animLayer.hasItems) animationLayers.push(animLayer); } } } - return { texture, foregroundTexture, color: planeColor, baseAlignBottom, foregroundAlignBottom, animationLayers }; + return { texture, foregroundTexture, color: planeColor, baseAlignBottom, foregroundAlignBottom, animationLayers, backgroundColor }; }; const planeData = getTextureAndColorForPlane(this._id, this._type, normal); @@ -453,8 +478,10 @@ export class RoomPlane implements IRoomPlane this._landscapeRenderWidth = width; this._landscapeRenderHeight = height; - this._animationCanvasWidth = renderMaxX; - this._animationCanvasHeight = renderMaxY; + // Use total landscape width for animation canvas, but always use actual height + // The renderMaxY is often too small (e.g., 160) while actual height is much larger (e.g., 755) + this._animationCanvasWidth = renderMaxX || width; + this._animationCanvasHeight = height; // Always use actual landscape height for animations this._landscapeOffsetX = renderOffsetX; this._landscapeOffsetY = renderOffsetY; @@ -475,6 +502,7 @@ export class RoomPlane implements IRoomPlane this._landscapeForegroundTint = landscapeTint; this._landscapeBaseAlignBottom = planeData.baseAlignBottom ?? false; this._landscapeForegroundAlignBottom = planeData.foregroundAlignBottom ?? false; + this._landscapeBackgroundColor = planeData.backgroundColor ?? null; this._planeSprite = new TilingSprite({ texture: Texture.WHITE, @@ -535,24 +563,37 @@ export class RoomPlane implements IRoomPlane if(needsUpdate || animationUpdate) { + // For landscapes with a custom backgroundColor, render it first + if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeBackgroundColor !== null) + { + this.renderBackgroundColor(); + } + GetRenderer().render({ target: this._planeTexture, container: this._planeSprite, transform: this.getMatrixForDimensions(this._planeSprite.width, this._planeSprite.height), - clear: true + clear: this._landscapeBackgroundColor === null }); - if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0 && this._hasWindowMask) - { - this.renderAnimationLayers(timeSinceStartMs, geometry); - } + // Layer order for landscapes: + // 1. Background color (rendered above) + // 2. Background texture + // 3. Animation layers (clouds) + // 4. Foreground texture if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeBackgroundTexture) { this.renderLandscapeLayer(this._landscapeBackgroundTexture, this._landscapeBackgroundTint, this._landscapeBaseAlignBottom); } - // Render foreground layer for landscapes on top of background/animation + // Render animation layers (clouds) - between background and foreground + if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0) + { + this.renderAnimationLayers(timeSinceStartMs, geometry); + } + + // Render foreground layer for landscapes on top of background and clouds if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeForegroundTexture) { this.renderLandscapeLayer(this._landscapeForegroundTexture, this._landscapeForegroundTint, this._landscapeForegroundAlignBottom); @@ -682,6 +723,35 @@ export class RoomPlane implements IRoomPlane layerSprite.destroy(); } + private renderBackgroundColor(): void + { + if(!this._planeTexture || this._landscapeBackgroundColor === null) return; + + const canvasWidth = this._landscapeRenderWidth; + const canvasHeight = this._landscapeRenderHeight; + + if(canvasWidth <= 0 || canvasHeight <= 0) return; + + // Create a solid color rectangle to fill the background + const colorGraphics = new Graphics(); + colorGraphics.rect(0, 0, canvasWidth, canvasHeight); + colorGraphics.fill(this._landscapeBackgroundColor); + + const colorContainer = new Container(); + colorContainer.addChild(colorGraphics); + + const transform = this.getMatrixForDimensions(canvasWidth, canvasHeight); + + GetRenderer().render({ + target: this._planeTexture, + container: colorContainer, + transform, + clear: true + }); + + colorGraphics.destroy(); + } + private updateCorners(geometry: IRoomGeometry): void { this._cornerA.assign(geometry.getScreenPosition(this._location));