From afd0a4fa1687889e2350b349ffe2d4cc25d10983 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 18:50:01 +0200 Subject: [PATCH] feat(room): RoomMessageHandler.applyFloorModelLocally for live floor preview Adds a public method that rebuilds the active room's floor geometry from an in-memory model string + wallHeight without touching the server. Same pipeline as the wire-driven onRoomModelEvent (FloorHeightMapMessageParser -> _planeParser -> wallGeometry), but instead of going through RoomEngine.createRoomInstance (which is a no-op on a room that already exists) it routes the resulting RoomMapData through the room object's ObjectRoomMapUpdateMessage channel - the same mechanism RoomPreviewer.updateRoomPlanes uses for its iso preview. Result: the visualization rebuilds in place and existing furniture/avatars are preserved. Refactor: extracted the parser->planeParser->wallGeometry- >RoomMapData work from onRoomModelEvent into a private _rebuildFloorGeometry(parser) helper so the wire handler and the new public method share an implementation. Intended use: tools that need a live in-room preview of a floor edit before committing it server-side (e.g. the React floor-plan editor's live-preview mode). The wire UpdateFloorPropertiesMessageComposer remains the source of truth - call applyFloorModelLocally only for transient client-side preview. --- packages/room/src/RoomMessageHandler.ts | 76 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/room/src/RoomMessageHandler.ts b/packages/room/src/RoomMessageHandler.ts index b97c6b6..634d6d6 100644 --- a/packages/room/src/RoomMessageHandler.ts +++ b/packages/room/src/RoomMessageHandler.ts @@ -2,11 +2,17 @@ import { AvatarGuideStatus, IConnection, IMessageEvent, IRoomCreator, IRoomObjec import { AreaHideMessageEvent, ConfInvisStateMessageEvent, DiceValueMessageEvent, FloorHeightMapEvent, FurnitureAliasesComposer, FurnitureAliasesEvent, FurnitureDataEvent, FurnitureFloorAddEvent, FurnitureFloorDataParser, FurnitureFloorEvent, FurnitureFloorRemoveEvent, FurnitureFloorUpdateEvent, FurnitureWallAddEvent, FurnitureWallDataParser, FurnitureWallEvent, FurnitureWallRemoveEvent, FurnitureWallUpdateEvent, GetCommunication, GetRoomEntryDataMessageComposer, GuideSessionEndedMessageEvent, GuideSessionErrorMessageEvent, GuideSessionStartedMessageEvent, IgnoreResultEvent, ItemDataUpdateMessageEvent, ObjectsDataUpdateEvent, ObjectsRollingEvent, OneWayDoorStatusMessageEvent, PetExperienceEvent, PetFigureUpdateEvent, RoomEntryTileMessageEvent, RoomEntryTileMessageParser, RoomHeightMapEvent, RoomHeightMapUpdateEvent, RoomPaintEvent, RoomReadyMessageEvent, RoomUnitChatEvent, RoomUnitChatShoutEvent, RoomUnitChatWhisperEvent, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitIdleEvent, RoomUnitInfoEvent, RoomUnitNumberEvent, RoomUnitRemoveEvent, RoomUnitStatusEvent, RoomUnitStatusMessage, RoomUnitTypingEvent, RoomVisualizationSettingsEvent, UserInfoEvent, WiredFurniMovementData, WiredMovementsEvent, WiredUserDirectionUpdateData, WiredUserMovementData, YouArePlayingGameEvent } from '@nitrots/communication'; import { GetRoomSessionManager, GetSessionDataManager } from '@nitrots/session'; import { Vector3d } from '@nitrots/utils'; +import { FloorHeightMapMessageParser } from '@nitrots/communication'; import { GetRoomEngine } from './GetRoomEngine'; import { RoomVariableEnum } from './RoomVariableEnum'; +import { ObjectRoomMapUpdateMessage } from './messages'; import { RoomPlaneParser } from './object/RoomPlaneParser'; import { FurnitureStackingHeightMap, LegacyWallGeometry } from './utils'; +// Local mirror of `RoomEngine.ROOM_OBJECT_ID` to avoid a circular +// import between this handler and the engine that owns it. +const ROOM_OWN_OBJECT_ID = -1; + type AreaHideControllerState = { rootX: number; rootY: number; @@ -232,9 +238,75 @@ export class RoomMessageHandler if(!parser) return; + const roomMap = this._rebuildFloorGeometry(parser); + + if(!roomMap) return; + + // Initial server-driven load: create the instance from + // scratch. RoomEngine.createRoomInstance is a no-op when + // the room already exists, so we never accidentally wipe + // furniture/avatars on a re-enter. + this._roomEngine.createRoomInstance(this._currentRoomId, roomMap); + } + + /** + * Apply a floor model to the ACTIVE room locally, without + * touching the server. Drives the same `wallGeometry` + + * `RoomPlaneParser` pipeline as the wire-driven path, then + * routes the resulting `RoomMapData` through the room object's + * `ObjectRoomMapUpdateMessage` channel — the same mechanism + * `RoomPreviewer.updateRoomPlanes` uses. The visualization + * rebuilds in place, so existing furniture and avatars are + * preserved. + * + * Intended for tools that need a live in-room preview of a + * floor edit before the user commits to a server save (e.g. + * the React floor-plan editor's live-preview mode). The wire + * `UpdateFloorPropertiesMessageComposer` is still the source + * of truth — call this purely for transient client-side + * preview, then send the composer separately when the user + * confirms. + * + * @returns `true` if the floor was rebuilt; `false` if no + * active room is bound, the engine isn't ready, or the + * model string failed to parse. + */ + public applyFloorModelLocally(modelString: string, wallHeight: number, scale: boolean = true): boolean + { + if(!this._roomEngine || this._currentRoomId <= 0 || !modelString) return false; + + const parser = new FloorHeightMapMessageParser(); + + parser.flush(); + + if(!parser.parseModel(modelString, wallHeight, scale)) return false; + + const roomMap = this._rebuildFloorGeometry(parser); + + if(!roomMap) return false; + + const roomObject = this._roomEngine.getRoomObject(this._currentRoomId, ROOM_OWN_OBJECT_ID, RoomObjectCategory.ROOM); + + if(!roomObject) return false; + + roomObject.processUpdateMessage(new ObjectRoomMapUpdateMessage(roomMap)); + + return true; + } + + /** + * Shared body of `onRoomModelEvent` and + * `applyFloorModelLocally`. Feeds the floor heightmap into + * `_planeParser`, refreshes `wallGeometry`, and returns the + * resulting `RoomMapData` (doors included). The caller decides + * whether to seed a fresh room (initial enter) or update an + * existing one (live preview). + */ + private _rebuildFloorGeometry(parser: FloorHeightMapMessageParser) + { const wallGeometry = this._roomEngine.getLegacyWallGeometry(this._currentRoomId); - if(!wallGeometry) return; + if(!wallGeometry) return null; this._planeParser.reset(); @@ -320,7 +392,7 @@ export class RoomMessageHandler dir: doorDirection }); - this._roomEngine.createRoomInstance(this._currentRoomId, roomMap); + return roomMap; } private onRoomHeightMapEvent(event: RoomHeightMapEvent): void