mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
🆕 Added Pickup furni to the floorplan
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { AddLinkEventTracker, convertNumbersForSaving, convertSettingToNumber, FloorHeightMapEvent, GetRoomEntryTileMessageComposer, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomEntryTileMessageEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
|
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 { 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 { LocalizeText, SendMessageComposer } from '../../api';
|
||||||
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||||
import { useMessageEvent, useNitroEvent } from '../../hooks';
|
import { useMessageEvent, useNitroEvent } from '../../hooks';
|
||||||
@@ -29,6 +29,7 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
const [ importExportVisible, setImportExportVisible ] = useState(false);
|
const [ importExportVisible, setImportExportVisible ] = useState(false);
|
||||||
const [ liveSync, setLiveSync ] = useState(true);
|
const [ liveSync, setLiveSync ] = useState(true);
|
||||||
const [ panMode, setPanMode ] = useState(false);
|
const [ panMode, setPanMode ] = useState(false);
|
||||||
|
const [ autoPickup, setAutoPickup ] = useState(false);
|
||||||
const { state, dispatch, loadFromServer, undo, redo, canUndo, canRedo } = useFloorplanReducer();
|
const { state, dispatch, loadFromServer, undo, redo, canUndo, canRedo } = useFloorplanReducer();
|
||||||
const originalRef = useRef<{
|
const originalRef = useRef<{
|
||||||
tilemap: string;
|
tilemap: string;
|
||||||
@@ -41,7 +42,7 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
|
|
||||||
const area = useMemo(() => areaCount(state.tiles), [ state.tiles ]);
|
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>(RoomEngineEvent.DISPOSED, () => setIsVisible(false));
|
useNitroEvent<RoomEngineEvent>(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', x: parser.x, y: parser.y, source: 'remote' });
|
||||||
dispatch({ type: 'SET_DOOR_DIR', dir: ((parser.direction | 0) & 7) as EntryDir, 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>(FloorHeightMapEvent, event =>
|
useMessageEvent<FloorHeightMapEvent>(FloorHeightMapEvent, event =>
|
||||||
@@ -110,6 +112,7 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
wallHeight: originalRef.current?.wallHeight ?? -1
|
wallHeight: originalRef.current?.wallHeight ?? -1
|
||||||
};
|
};
|
||||||
dispatch({ type: 'SET_THICKNESS', wall, floor, source: 'remote' });
|
dispatch({ type: 'SET_THICKNESS', wall, floor, source: 'remote' });
|
||||||
|
mergeBaseline({ thicknessWall: wall, thicknessFloor: floor });
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@@ -173,7 +176,8 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
state.door.dir,
|
state.door.dir,
|
||||||
convertNumbersForSaving(state.thickness.wall),
|
convertNumbersForSaving(state.thickness.wall),
|
||||||
convertNumbersForSaving(state.thickness.floor),
|
convertNumbersForSaving(state.thickness.floor),
|
||||||
state.wallHeight - 1
|
state.wallHeight - 1,
|
||||||
|
autoPickup
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,7 +228,17 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
<Flex
|
<Flex
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={ 1 }
|
gap={ 1 }
|
||||||
className={ `ml-auto border rounded px-2 py-1 cursor-pointer select-none ${ liveSync ? 'bg-emerald-500/15 border-emerald-500 text-emerald-700' : 'border-zinc-400 text-zinc-600' }` }
|
className={ `ml-auto border rounded px-2 py-1 cursor-pointer select-none ${ autoPickup ? 'bg-amber-500/15 border-amber-500 text-amber-700' : 'border-zinc-400 text-zinc-600' }` }
|
||||||
|
onClick={ () => setAutoPickup(v => !v) }
|
||||||
|
title="On save: pick up furniture blocking the new floor plan and return it to its owner's inventory"
|
||||||
|
>
|
||||||
|
<FaBoxOpen className={ autoPickup ? 'text-amber-600' : 'text-zinc-500' } />
|
||||||
|
<Text bold small>{ autoPickup ? 'Pick up blocking furni ON' : 'Pick up blocking furni OFF' }</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
alignItems="center"
|
||||||
|
gap={ 1 }
|
||||||
|
className={ `border rounded px-2 py-1 cursor-pointer select-none ${ liveSync ? 'bg-emerald-500/15 border-emerald-500 text-emerald-700' : 'border-zinc-400 text-zinc-600' }` }
|
||||||
onClick={ () => setLiveSync(v => !v) }
|
onClick={ () => setLiveSync(v => !v) }
|
||||||
title="Local in-room preview while drawing (does not save to server)"
|
title="Local in-room preview while drawing (does not save to server)"
|
||||||
>
|
>
|
||||||
@@ -256,7 +270,8 @@ export const FloorplanEditorView: FC = () =>
|
|||||||
state.door.dir,
|
state.door.dir,
|
||||||
convertNumbersForSaving(state.thickness.wall),
|
convertNumbersForSaving(state.thickness.wall),
|
||||||
convertNumbersForSaving(state.thickness.floor),
|
convertNumbersForSaving(state.thickness.floor),
|
||||||
state.wallHeight - 1
|
state.wallHeight - 1,
|
||||||
|
autoPickup
|
||||||
));
|
));
|
||||||
} }
|
} }
|
||||||
onRevertText={ () => originalRef.current?.tilemap ?? serializeTilemap(state.tiles) }
|
onRevertText={ () => originalRef.current?.tilemap ?? serializeTilemap(state.tiles) }
|
||||||
|
|||||||
@@ -1,44 +1,18 @@
|
|||||||
import { GetRoomEngine, GetRoomMessageHandler } from '@nitrots/nitro-renderer';
|
import { GetRoomEngine, GetRoomMessageHandler } from '@nitrots/nitro-renderer';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
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 { FloorplanState } from '../../../components/floorplan-editor/state/types';
|
||||||
import { useActiveRoomSessionSnapshot } from '../../session/useSessionSnapshots';
|
import { useActiveRoomSessionSnapshot } from '../../session/useSessionSnapshots';
|
||||||
|
|
||||||
/**
|
const normalizeTilemap = (raw: string): string => serializeTilemap(parseTilemap(raw));
|
||||||
* 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).
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type LivePreviewPayload = {
|
export type LivePreviewPayload = {
|
||||||
/** Newline-or-CR-separated tilemap (the renderer parser accepts \r). */
|
|
||||||
tilemap: string;
|
tilemap: string;
|
||||||
doorX: number;
|
doorX: number;
|
||||||
doorY: number;
|
doorY: number;
|
||||||
doorDir: number;
|
doorDir: number;
|
||||||
/** Editor-space (0..3). */
|
|
||||||
thicknessWall: number;
|
thicknessWall: number;
|
||||||
thicknessFloor: number;
|
thicknessFloor: number;
|
||||||
/** Editor-space (1..N). Server space is `wallHeight - 1`. */
|
|
||||||
wallHeight: number;
|
wallHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,17 +22,8 @@ export type UseFloorplanLiveSyncOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseFloorplanLiveSyncApi = {
|
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;
|
setBaseline: (payload: LivePreviewPayload) => void;
|
||||||
/**
|
mergeBaseline: (partial: Partial<LivePreviewPayload>) => void;
|
||||||
* Restore the in-room preview to the recorded baseline.
|
|
||||||
* Use when the user closes the editor without saving.
|
|
||||||
*/
|
|
||||||
revert: () => void;
|
revert: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,7 +86,6 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo
|
|||||||
|
|
||||||
const baselineRef = useRef<LivePreviewPayload | null>(null);
|
const baselineRef = useRef<LivePreviewPayload | null>(null);
|
||||||
const lastAppliedRef = useRef<LivePreviewPayload | null>(null);
|
const lastAppliedRef = useRef<LivePreviewPayload | null>(null);
|
||||||
const wasEnabledRef = useRef(false);
|
|
||||||
|
|
||||||
const { tiles, door, thickness, wallHeight } = state;
|
const { tiles, door, thickness, wallHeight } = state;
|
||||||
const currentPayload = useMemo<LivePreviewPayload>(() => ({
|
const currentPayload = useMemo<LivePreviewPayload>(() => ({
|
||||||
@@ -136,8 +100,29 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo
|
|||||||
|
|
||||||
const setBaseline = useCallback((payload: LivePreviewPayload) =>
|
const setBaseline = useCallback((payload: LivePreviewPayload) =>
|
||||||
{
|
{
|
||||||
baselineRef.current = payload;
|
const normalized: LivePreviewPayload = {
|
||||||
lastAppliedRef.current = payload;
|
...payload,
|
||||||
|
tilemap: normalizeTilemap(payload.tilemap)
|
||||||
|
};
|
||||||
|
|
||||||
|
baselineRef.current = normalized;
|
||||||
|
lastAppliedRef.current = normalized;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mergeBaseline = useCallback((partial: Partial<LivePreviewPayload>) =>
|
||||||
|
{
|
||||||
|
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(() =>
|
const revert = useCallback(() =>
|
||||||
@@ -151,23 +136,15 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(!enabled)
|
if(!enabled) return;
|
||||||
{
|
|
||||||
wasEnabledRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!baselineRef.current) return;
|
if(!baselineRef.current) return;
|
||||||
|
|
||||||
const isFirstEnable = !wasEnabledRef.current;
|
|
||||||
wasEnabledRef.current = true;
|
|
||||||
|
|
||||||
const previous = lastAppliedRef.current;
|
const previous = lastAppliedRef.current;
|
||||||
|
|
||||||
if(!isFirstEnable && previous && livePreviewPayloadsEqual(currentPayload, previous)) return;
|
if(previous && livePreviewPayloadsEqual(currentPayload, previous)) return;
|
||||||
|
|
||||||
if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload;
|
if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload;
|
||||||
}, [ enabled, currentPayload, roomId ]);
|
}, [ enabled, currentPayload, roomId ]);
|
||||||
|
|
||||||
return { setBaseline, revert };
|
return { setBaseline, mergeBaseline, revert };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user