mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge branch 'duckietm:main' into feat/navigator-modernization
This commit is contained in:
@@ -12,35 +12,51 @@ export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
|
||||
headOnly?: boolean;
|
||||
direction?: number;
|
||||
scale?: number;
|
||||
fit?: boolean;
|
||||
}
|
||||
|
||||
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
|
||||
{
|
||||
const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
|
||||
const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, fit = false, classNames = [], style = {}, ...rest } = props;
|
||||
const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
|
||||
const [ isReady, setIsReady ] = useState<boolean>(false);
|
||||
const isDisposed = useRef(false);
|
||||
// Request id bumped on every prop change. The SDK can call
|
||||
// resetFigure asynchronously when server-side figure data lands;
|
||||
// if props change in quick succession the older callback could
|
||||
// otherwise overwrite the newer image. The closure captures the
|
||||
// id and bails when stale.
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
const getClassNames = useMemo(() =>
|
||||
{
|
||||
const newClassNames: string[] = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ];
|
||||
let newClassNames: string[];
|
||||
|
||||
if(fit)
|
||||
{
|
||||
newClassNames = [ 'avatar-image absolute inset-0 pointer-events-none' ];
|
||||
}
|
||||
else if(headOnly)
|
||||
{
|
||||
newClassNames = [ 'avatar-image absolute inset-0 bg-no-repeat pointer-events-none' ];
|
||||
}
|
||||
else
|
||||
{
|
||||
newClassNames = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat left-[-2px] pointer-events-none' ];
|
||||
}
|
||||
|
||||
if(classNames.length) newClassNames.push(...classNames);
|
||||
|
||||
return newClassNames;
|
||||
}, [ classNames ]);
|
||||
}, [ classNames, headOnly, fit ]);
|
||||
|
||||
const getStyle = useMemo(() =>
|
||||
{
|
||||
let newStyle: CSSProperties = {};
|
||||
|
||||
if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
|
||||
if(!fit && avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
|
||||
|
||||
if(headOnly && !fit)
|
||||
{
|
||||
newStyle.backgroundSize = '130px auto';
|
||||
newStyle.backgroundPosition = '51% 40%';
|
||||
newStyle.imageRendering = 'pixelated';
|
||||
}
|
||||
|
||||
if(scale !== 1)
|
||||
{
|
||||
@@ -52,7 +68,7 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
|
||||
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
|
||||
|
||||
return newStyle;
|
||||
}, [ avatarUrl, scale, style ]);
|
||||
}, [ avatarUrl, scale, style, headOnly, fit ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -116,5 +132,17 @@ export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
|
||||
return (
|
||||
<Base classNames={ getClassNames } style={ getStyle } { ...rest }>
|
||||
{ fit && avatarUrl && avatarUrl.length > 0 && (
|
||||
<img
|
||||
src={ avatarUrl }
|
||||
alt=""
|
||||
draggable={ false }
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
style={ { imageRendering: 'pixelated', transform: 'translateY(-20%)' } }
|
||||
/>
|
||||
) }
|
||||
</Base>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
{ iconUrl && !(offer.product.productType === ProductTypeEnum.ROBOT) &&
|
||||
<div className="nitro-catalog-classic-grid-offer-icon" style={ { backgroundImage: `url(${ iconUrl })` } } /> }
|
||||
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
|
||||
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ offer.product.extraParam } fit /> }
|
||||
<div
|
||||
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
|
||||
onClick={ e =>
|
||||
|
||||
@@ -88,7 +88,6 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
||||
</div>
|
||||
</div> }
|
||||
|
||||
{ /* Welcome/description card */ }
|
||||
{ !currentOffer &&
|
||||
<div className="nitro-catalog-classic-welcome flex items-center gap-3">
|
||||
{ !!page.localization.getImage(1) &&
|
||||
@@ -96,11 +95,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
||||
<Text className="text-[11px]! text-muted" dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
|
||||
</div> }
|
||||
|
||||
{ /* Item grid */ }
|
||||
<div className="nitro-catalog-classic-grid-shell flex-1 overflow-auto min-h-0">
|
||||
{ GetConfigurationValue('catalog.headers') &&
|
||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||
<CatalogItemGridWidgetView className="nitro-catalog-classic-grid" columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } />
|
||||
<CatalogItemGridWidgetView className="nitro-catalog-classic-grid" columnCount={ 7 } columnMinHeight={ currentPage.layoutCode === 'bots' ? 65 : 50 } columnMinWidth={ currentPage.layoutCode === 'bots' ? 65 : 50 } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>(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>(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 = () =>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
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) }
|
||||
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) }
|
||||
|
||||
@@ -38,8 +38,8 @@ export const InventoryBotItemView: FC<PropsWithChildren<{
|
||||
};
|
||||
|
||||
return (
|
||||
<InfiniteGrid.Item itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onDoubleClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest } className="*:[background-position-y:-32px]">
|
||||
<LayoutAvatarImageView direction={ 3 } figure={ botItem.botData.figure } headOnly={ true } />
|
||||
<InfiniteGrid.Item itemActive={ (selectedBot === botItem) } itemUnseen={ unseen } onDoubleClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest } className="aspect-[2/3]">
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ botItem.botData.figure } fit />
|
||||
{ children }
|
||||
</InfiniteGrid.Item>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ export const InventoryBotView: FC<{
|
||||
<div className="grid h-full grid-cols-12 gap-2">
|
||||
<div className="flex flex-col col-span-7 gap-1 overflow-hidden">
|
||||
<InfiniteGrid<IBotItem>
|
||||
columnCount={ 6 }
|
||||
columnCount={ 4 }
|
||||
itemRender={ item => <InventoryBotItemView botItem={ item } /> }
|
||||
items={ botItems } />
|
||||
</div>
|
||||
|
||||
@@ -69,10 +69,10 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
toggleTimeoutRef.current = setTimeout(() => { toggleLockRef.current = false; }, TOGGLE_LOCK_MS);
|
||||
}, []);
|
||||
|
||||
const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1540px]:bottom-0' : 'bottom-0';
|
||||
const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1540px]:hidden';
|
||||
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:block';
|
||||
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1540px]:flex';
|
||||
const compactFramePosition = (isToolbarOpen && isInRoom) ? 'bottom-[90px] min-[1700px]:bottom-0' : 'bottom-0';
|
||||
const mobileOnlyClasses = isTouchLayout ? '' : 'min-[1700px]:hidden';
|
||||
const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:block';
|
||||
const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden min-[1700px]:flex';
|
||||
const leftNavVariants = useMemo<Variants>(() => ({
|
||||
hidden: { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8, pointerEvents: 'none' },
|
||||
visible: { opacity: 1, x: 0, y: 0, pointerEvents: 'auto' }
|
||||
@@ -216,14 +216,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
|
||||
{ (getFullCount > 0) &&
|
||||
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<AnimatePresence>
|
||||
{ isMeExpanded &&
|
||||
@@ -237,7 +229,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className="cursor-pointer"
|
||||
className="cursor-pointer relative h-[40px] w-[40px] overflow-hidden"
|
||||
whileHover={ { scale: 1.08 } }
|
||||
whileTap={ { scale: 0.95 } }
|
||||
onClick={ event =>
|
||||
@@ -245,11 +237,19 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
setMeExpanded(value => !value);
|
||||
event.stopPropagation();
|
||||
} }>
|
||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[64px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: "8px" } } />
|
||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon" style={ { backgroundSize: 'auto', backgroundPosition: '-25px -38px' } } />
|
||||
</motion.div>
|
||||
{ (getTotalUnseen > 0) &&
|
||||
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="inventory" onClick={ () => CreateLinkEvent('inventory/toggle') } className="tb-icon" />
|
||||
{ (getFullCount > 0) &&
|
||||
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
|
||||
</motion.div>
|
||||
{ (isInRoom && showToolbarButton) &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="wired-tools" onClick={ openMonitor } className="tb-icon" />
|
||||
@@ -268,14 +268,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ (openTicketsCount > 0) &&
|
||||
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||
</motion.div> }
|
||||
{ isMod &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
{ (isHk && hkEnabled) &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
{ isMod &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
@@ -324,6 +324,32 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="catalog" onClick={ () => CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" />
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative shrink-0">
|
||||
<AnimatePresence>
|
||||
{ isMeExpanded &&
|
||||
<motion.div
|
||||
initial={ { opacity: 0, y: 6, scale: 0.97 } }
|
||||
animate={ { opacity: 1, y: 0, scale: 1 } }
|
||||
exit={ { opacity: 0, y: 6, scale: 0.97 } }
|
||||
transition={ ME_POPOVER_TRANSITION }
|
||||
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
|
||||
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className="cursor-pointer relative h-[40px] w-[40px] overflow-hidden"
|
||||
whileHover={ { scale: 1.08 } }
|
||||
whileTap={ { scale: 0.95 } }
|
||||
onClick={ event =>
|
||||
{
|
||||
setMeExpanded(value => !value);
|
||||
event.stopPropagation();
|
||||
} }>
|
||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon" style={ { backgroundSize: 'auto', backgroundPosition: '-25px -38px' } } />
|
||||
</motion.div>
|
||||
{ (getTotalUnseen > 0) &&
|
||||
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="buildersclub" onClick={ () => CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" />
|
||||
</motion.div>
|
||||
@@ -333,32 +359,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
<LayoutItemCountView count={ getFullCount } className="absolute -right-1 top-0" /> }
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<motion.div variants={ itemVariants } className="relative mx-[2px] shrink-0">
|
||||
<AnimatePresence>
|
||||
{ isMeExpanded &&
|
||||
<motion.div
|
||||
initial={ { opacity: 0, y: 6, scale: 0.97 } }
|
||||
animate={ { opacity: 1, y: 0, scale: 1 } }
|
||||
exit={ { opacity: 0, y: 6, scale: 0.97 } }
|
||||
transition={ ME_POPOVER_TRANSITION }
|
||||
className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2">
|
||||
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className="cursor-pointer"
|
||||
whileHover={ { scale: 1.08 } }
|
||||
whileTap={ { scale: 0.95 } }
|
||||
onClick={ event =>
|
||||
{
|
||||
setMeExpanded(value => !value);
|
||||
event.stopPropagation();
|
||||
} }>
|
||||
<LayoutAvatarImageView headOnly={ true } direction={ 2 } figure={ userFigure } className="tb-icon !h-[64px] !w-[32px] !bg-center !bg-no-repeat" style={ { marginTop: "8px" } } />
|
||||
</motion.div>
|
||||
{ (getTotalUnseen > 0) &&
|
||||
<LayoutItemCountView count={ getTotalUnseen } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||
</motion.div>
|
||||
<motion.div
|
||||
variants={ containerVariants }
|
||||
className="tb-bar-scroll flex h-full items-center gap-2 overflow-x-auto overflow-y-visible px-1">
|
||||
@@ -380,14 +380,14 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
|
||||
{ (openTicketsCount > 0) &&
|
||||
<LayoutItemCountView count={ openTicketsCount } className="pointer-events-none absolute -right-1 -top-1 z-10" /> }
|
||||
</motion.div> }
|
||||
{ isMod &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
{ (isHk && hkEnabled) &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="housekeeping" onClick={ () => CreateLinkEvent('housekeeping/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
{ isMod &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<ToolbarItemView icon="furnieditor" onClick={ () => CreateLinkEvent('furni-editor/toggle') } className="tb-icon" />
|
||||
</motion.div> }
|
||||
<motion.div variants={ itemVariants } className="relative">
|
||||
<ToolbarItemView icon="friendall" onClick={ () => CreateLinkEvent('friends/toggle') } className="tb-icon" />
|
||||
{ (requests.length > 0) &&
|
||||
|
||||
@@ -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<LivePreviewPayload>) => void;
|
||||
revert: () => void;
|
||||
};
|
||||
|
||||
@@ -122,8 +87,6 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo
|
||||
const baselineRef = useRef<LivePreviewPayload | null>(null);
|
||||
const lastAppliedRef = useRef<LivePreviewPayload | null>(null);
|
||||
|
||||
// Destructure first so the memo deps stay precise without
|
||||
// triggering exhaustive-deps on `state` as a whole.
|
||||
const { tiles, door, thickness, wallHeight } = state;
|
||||
const currentPayload = useMemo<LivePreviewPayload>(() => ({
|
||||
tilemap: serializeTilemap(tiles),
|
||||
@@ -137,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<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(() =>
|
||||
@@ -150,21 +134,17 @@ export const useFloorplanLiveSync = (opts: UseFloorplanLiveSyncOptions): UseFloo
|
||||
if(applyToRenderer(baseline, roomId)) lastAppliedRef.current = baseline;
|
||||
}, [ roomId ]);
|
||||
|
||||
// Apply the current payload to the renderer whenever it
|
||||
// diverges from what's already in the room. Synchronous + no
|
||||
// debounce — the renderer pipeline is fast enough that every
|
||||
// brush stroke can land a paint.
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!enabled) return;
|
||||
if(!baselineRef.current) return;
|
||||
|
||||
const previous = lastAppliedRef.current;
|
||||
|
||||
if(previous && livePreviewPayloadsEqual(currentPayload, previous)) return;
|
||||
if(!previous && !baselineRef.current) return;
|
||||
|
||||
if(applyToRenderer(currentPayload, roomId)) lastAppliedRef.current = currentPayload;
|
||||
}, [ enabled, currentPayload, roomId ]);
|
||||
|
||||
return { setBaseline, revert };
|
||||
return { setBaseline, mergeBaseline, revert };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user