diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 07e721e..8094d1b 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,6 +1,6 @@ import { AddLinkEventTracker, convertNumbersForSaving, convertSettingToNumber, FloorHeightMapEvent, GetRoomEntryTileMessageComposer, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEntryTileMessageEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { FaBolt, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FaBolt, FaBoxOpen, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; @@ -29,6 +29,7 @@ export const FloorplanEditorView: FC = () => const [ importExportVisible, setImportExportVisible ] = useState(false); const [ liveSync, setLiveSync ] = useState(true); const [ panMode, setPanMode ] = useState(false); + const [ autoPickup, setAutoPickup ] = useState(false); const { state, dispatch, loadFromServer, undo, redo, canUndo, canRedo } = useFloorplanReducer(); const originalRef = useRef<{ tilemap: string; @@ -41,7 +42,7 @@ export const FloorplanEditorView: FC = () => const area = useMemo(() => areaCount(state.tiles), [ state.tiles ]); - const { setBaseline, revert: revertLivePreview } = useFloorplanLiveSync({ enabled: liveSync && isVisible, state }); + const { setBaseline, mergeBaseline, revert: revertLivePreview } = useFloorplanLiveSync({ enabled: liveSync && isVisible, state }); useNitroEvent(RoomEngineEvent.DISPOSED, () => setIsVisible(false)); @@ -64,6 +65,7 @@ export const FloorplanEditorView: FC = () => }; dispatch({ type: 'SET_DOOR', x: parser.x, y: parser.y, source: 'remote' }); dispatch({ type: 'SET_DOOR_DIR', dir: ((parser.direction | 0) & 7) as EntryDir, source: 'remote' }); + mergeBaseline({ doorX: parser.x, doorY: parser.y, doorDir: (parser.direction | 0) & 7 }); }); useMessageEvent(FloorHeightMapEvent, event => @@ -110,6 +112,7 @@ export const FloorplanEditorView: FC = () => wallHeight: originalRef.current?.wallHeight ?? -1 }; dispatch({ type: 'SET_THICKNESS', wall, floor, source: 'remote' }); + mergeBaseline({ thicknessWall: wall, thicknessFloor: floor }); }); useEffect(() => @@ -173,7 +176,8 @@ export const FloorplanEditorView: FC = () => state.door.dir, convertNumbersForSaving(state.thickness.wall), convertNumbersForSaving(state.thickness.floor), - state.wallHeight - 1 + state.wallHeight - 1, + autoPickup )); }; @@ -224,7 +228,17 @@ export const FloorplanEditorView: FC = () => setAutoPickup(v => !v) } + title="On save: pick up furniture blocking the new floor plan and return it to its owner's inventory" + > + + { autoPickup ? 'Pick up blocking furni ON' : 'Pick up blocking furni OFF' } + + setLiveSync(v => !v) } title="Local in-room preview while drawing (does not save to server)" > @@ -256,7 +270,8 @@ export const FloorplanEditorView: FC = () => state.door.dir, convertNumbersForSaving(state.thickness.wall), convertNumbersForSaving(state.thickness.floor), - state.wallHeight - 1 + state.wallHeight - 1, + autoPickup )); } } onRevertText={ () => originalRef.current?.tilemap ?? serializeTilemap(state.tiles) } diff --git a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts index ed3ae72..9dbfdfa 100644 --- a/src/hooks/rooms/widgets/useFloorplanLiveSync.ts +++ b/src/hooks/rooms/widgets/useFloorplanLiveSync.ts @@ -1,44 +1,18 @@ import { GetRoomEngine, GetRoomMessageHandler } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { serializeTilemap } from '../../../components/floorplan-editor/state/encoding'; +import { parseTilemap, serializeTilemap } from '../../../components/floorplan-editor/state/encoding'; import { FloorplanState } from '../../../components/floorplan-editor/state/types'; import { useActiveRoomSessionSnapshot } from '../../session/useSessionSnapshots'; -/** - * Client-side live preview for the floor-plan editor. - * - * Every tile / door / thickness / wallHeight change in the editor - * is applied IMMEDIATELY to the 3D room behind the editor card - * via the renderer's local `RoomMessageHandler.applyFloorModelLocally` - * (added in the renderer's `feat/floorplan-live-preview` branch). - * Nothing is sent to the server until the user explicitly clicks - * Save — at that point `FloorplanEditorView` fires the - * `UpdateFloorPropertiesMessageComposer` directly. - * - * Closing the editor without saving leaves the live preview - * in place visually. To restore the pre-edit room, call `revert` - * — it re-applies the baseline payload locally. The next - * `FloorHeightMapEvent` from the server (e.g. on room re-enter) - * also wins and overwrites whatever preview is in place. - * - * Thickness changes additionally call - * `RoomEngine.updateRoomInstancePlaneThickness` for zero-latency - * wall/floor depth feedback (the full geometry rebuild that - * `applyFloorModelLocally` performs already reflects the new - * thickness in its plane data, but the dedicated thickness - * setter is cheaper and updates instantly as a slider is dragged). - */ +const normalizeTilemap = (raw: string): string => serializeTilemap(parseTilemap(raw)); export type LivePreviewPayload = { - /** Newline-or-CR-separated tilemap (the renderer parser accepts \r). */ tilemap: string; doorX: number; doorY: number; doorDir: number; - /** Editor-space (0..3). */ thicknessWall: number; thicknessFloor: number; - /** Editor-space (1..N). Server space is `wallHeight - 1`. */ wallHeight: number; }; @@ -48,17 +22,8 @@ export type UseFloorplanLiveSyncOptions = { }; export type UseFloorplanLiveSyncApi = { - /** - * Mark a payload as "currently shown in the room" so subsequent - * state diffs are computed against it. Editors call this on - * every server-driven snapshot push (FloorHeightMapEvent, - * RoomVisualizationSettingsEvent, …). - */ setBaseline: (payload: LivePreviewPayload) => void; - /** - * Restore the in-room preview to the recorded baseline. - * Use when the user closes the editor without saving. - */ + mergeBaseline: (partial: Partial) => void; revert: () => void; }; @@ -121,7 +86,6 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo const baselineRef = useRef(null); const lastAppliedRef = useRef(null); - const wasEnabledRef = useRef(false); const { tiles, door, thickness, wallHeight } = state; const currentPayload = useMemo(() => ({ @@ -136,8 +100,29 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo const setBaseline = useCallback((payload: LivePreviewPayload) => { - baselineRef.current = payload; - lastAppliedRef.current = payload; + const normalized: LivePreviewPayload = { + ...payload, + tilemap: normalizeTilemap(payload.tilemap) + }; + + baselineRef.current = normalized; + lastAppliedRef.current = normalized; + }, []); + + const mergeBaseline = useCallback((partial: Partial) => + { + const previous = baselineRef.current; + + if(!previous) return; + + const next: LivePreviewPayload = { + ...previous, + ...partial, + tilemap: partial.tilemap !== undefined ? normalizeTilemap(partial.tilemap) : previous.tilemap + }; + + baselineRef.current = next; + lastAppliedRef.current = next; }, []); const revert = useCallback(() => @@ -151,23 +136,15 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo useEffect(() => { - if(!enabled) - { - wasEnabledRef.current = false; - return; - } - + if(!enabled) return; if(!baselineRef.current) return; - const isFirstEnable = !wasEnabledRef.current; - wasEnabledRef.current = true; - const previous = lastAppliedRef.current; - if(!isFirstEnable && previous && livePreviewPayloadsEqual(currentPayload, previous)) return; + if(previous && livePreviewPayloadsEqual(currentPayload, previous)) return; if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload; }, [ enabled, currentPayload, roomId ]); - return { setBaseline, revert }; + return { setBaseline, mergeBaseline, revert }; };