mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
feat(floorplan): add real-time 3D preview to floor plan editor
- Add FloorplanPreviewView with live isometric room preview - Add FloorplanHeightSelector component for height picking - Refactor FloorplanOptionsView for cleaner layout - Update FloorplanEditorContext with preview state management - Improve FloorplanCanvasView rendering
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
|
||||
import { LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useMessageEvent, useNitroEvent } from '../../hooks';
|
||||
import { FloorplanEditorContextProvider } from './FloorplanEditorContext';
|
||||
import { FloorplanEditor } from '@nitrots/nitro-renderer';
|
||||
import { IFloorplanSettings } from '@nitrots/nitro-renderer';
|
||||
import { IVisualizationSettings } from '@nitrots/nitro-renderer';
|
||||
import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer';
|
||||
import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer';
|
||||
import { FloorplanCanvasView } from './views/FloorplanCanvasView';
|
||||
import { FloorplanImportExportView } from './views/FloorplanImportExportView';
|
||||
import { FloorplanOptionsView } from './views/FloorplanOptionsView';
|
||||
import { FloorplanHeightSelector } from './views/FloorplanHeightSelector';
|
||||
import { FloorplanPreviewView } from './views/FloorplanPreviewView';
|
||||
|
||||
|
||||
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
||||
const MIN_WALL_HEIGHT = 0;
|
||||
const MAX_WALL_HEIGHT = 16;
|
||||
|
||||
export const FloorplanEditorView: FC<{}> = props =>
|
||||
{
|
||||
@@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props =>
|
||||
thicknessWall: 1,
|
||||
thicknessFloor: 1
|
||||
});
|
||||
const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null);
|
||||
const [ floorHeight, setFloorHeight ] = useState(0);
|
||||
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
|
||||
const [ tilemapVersion, setTilemapVersion ] = useState(0);
|
||||
const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 });
|
||||
|
||||
const calculateArea = useCallback(() =>
|
||||
{
|
||||
const tilemap = FloorplanEditor.instance.tilemap;
|
||||
|
||||
if(!tilemap || tilemap.length === 0)
|
||||
{
|
||||
setAreaInfo({ total: 0, walkable: 0 });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let walkable = 0;
|
||||
|
||||
for(let y = 0; y < tilemap.length; y++)
|
||||
{
|
||||
if(!tilemap[y]) continue;
|
||||
|
||||
for(let x = 0; x < tilemap[y].length; x++)
|
||||
{
|
||||
if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue;
|
||||
|
||||
total++;
|
||||
|
||||
if(!tilemap[y][x].isBlocked) walkable++;
|
||||
}
|
||||
}
|
||||
|
||||
setAreaInfo({ total, walkable });
|
||||
}, []);
|
||||
|
||||
// sync floorHeight/floorAction changes to the FloorplanEditor instance
|
||||
useEffect(() =>
|
||||
{
|
||||
FloorplanEditor.instance.actionSettings.currentAction = floorAction;
|
||||
FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36);
|
||||
}, [ floorHeight, floorAction ]);
|
||||
|
||||
// register onTilemapChange callback
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible) return;
|
||||
|
||||
FloorplanEditor.instance.onTilemapChange = () =>
|
||||
{
|
||||
setTilemapVersion(prev => prev + 1);
|
||||
calculateArea();
|
||||
};
|
||||
|
||||
return () =>
|
||||
{
|
||||
FloorplanEditor.instance.onTilemapChange = null;
|
||||
};
|
||||
}, [ isVisible, calculateArea ]);
|
||||
|
||||
const saveFloorChanges = () =>
|
||||
{
|
||||
@@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props =>
|
||||
convertNumbersForSaving(visualizationSettings.thicknessFloor),
|
||||
(visualizationSettings.wallHeight - 1)
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const revertChanges = () =>
|
||||
{
|
||||
setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir });
|
||||
|
||||
|
||||
FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] };
|
||||
FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles);
|
||||
FloorplanEditor.instance.renderTiles();
|
||||
}
|
||||
};
|
||||
|
||||
const onWallHeightChange = (value: number) =>
|
||||
{
|
||||
if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
|
||||
|
||||
if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
|
||||
|
||||
setVisualizationSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.wallHeight = value;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const increaseWallHeight = () =>
|
||||
{
|
||||
let height = (visualizationSettings.wallHeight + 1);
|
||||
|
||||
if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
|
||||
|
||||
onWallHeightChange(height);
|
||||
};
|
||||
|
||||
const decreaseWallHeight = () =>
|
||||
{
|
||||
let height = (visualizationSettings.wallHeight - 1);
|
||||
|
||||
if(height <= 0) height = MIN_WALL_HEIGHT;
|
||||
|
||||
onWallHeightChange(height);
|
||||
};
|
||||
|
||||
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.DISPOSED, event => setIsVisible(false));
|
||||
|
||||
@@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props =>
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
@@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props =>
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FloorplanEditorContextProvider value={ { originalFloorplanSettings: originalFloorplanSettings, setOriginalFloorplanSettings: setOriginalFloorplanSettings, visualizationSettings: visualizationSettings, setVisualizationSettings: setVisualizationSettings } }>
|
||||
<FloorplanEditorContextProvider value={ {
|
||||
originalFloorplanSettings,
|
||||
setOriginalFloorplanSettings,
|
||||
visualizationSettings,
|
||||
setVisualizationSettings,
|
||||
floorHeight,
|
||||
setFloorHeight,
|
||||
floorAction,
|
||||
setFloorAction,
|
||||
tilemapVersion,
|
||||
areaInfo
|
||||
} }>
|
||||
{ isVisible &&
|
||||
<NitroCardView uniqueKey="floorpan-editor" className="w-[760px] h-[500px]" theme="primary-slim">
|
||||
<NitroCardView uniqueKey="floorpan-editor" className="w-[1100px] h-[600px]" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('floor.plan.editor.title') } onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardContentView overflow="hidden">
|
||||
<FloorplanOptionsView onCanvasScroll={ direction => canvasScrollHandler && canvasScrollHandler(direction) } />
|
||||
<FloorplanCanvasView overflow="hidden" setScrollHandler={ setCanvasScrollHandler } />
|
||||
<NitroCardContentView overflow="hidden" className="flex flex-col">
|
||||
<FloorplanOptionsView />
|
||||
<Flex gap={ 2 } className="flex-1 min-h-0">
|
||||
<FloorplanHeightSelector />
|
||||
<FloorplanCanvasView overflow="hidden" />
|
||||
<Column gap={ 2 } className="w-[380px] min-w-[380px]">
|
||||
<FloorplanPreviewView />
|
||||
<Flex gap={ 1 } alignItems="center">
|
||||
<Text bold small>{ LocalizeText('floor.editor.wall.height') }</Text>
|
||||
<FaCaretLeft className="cursor-pointer fa-icon" onClick={ decreaseWallHeight } />
|
||||
<input type="number" className="form-control form-control-sm w-[49px]" value={ visualizationSettings.wallHeight } onChange={ event => onWallHeightChange(event.target.valueAsNumber) } />
|
||||
<FaCaretRight className="cursor-pointer fa-icon" onClick={ increaseWallHeight } />
|
||||
</Flex>
|
||||
<Text bold small className="text-center">
|
||||
Area: { areaInfo.total } ({ areaInfo.walkable } caselle)
|
||||
</Text>
|
||||
</Column>
|
||||
</Flex>
|
||||
<Flex justifyContent="between">
|
||||
<Button onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
|
||||
<Button variant="danger" onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
|
||||
<ButtonGroup>
|
||||
<Button disabled={ true }>{ LocalizeText('floor.plan.editor.preview') }</Button>
|
||||
<Button onClick={ event => setImportExportVisible(true) }>{ LocalizeText('floor.plan.editor.import.export') }</Button>
|
||||
<Button onClick={ saveFloorChanges }>{ LocalizeText('floor.plan.editor.save') }</Button>
|
||||
</ButtonGroup>
|
||||
@@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props =>
|
||||
<FloorplanImportExportView onCloseClick={ () => setImportExportVisible(false) } /> }
|
||||
</FloorplanEditorContextProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user