Add real-time 3D preview to floor plan editor

Redesign the floor plan editor with side-by-side layout featuring:
- Real-time isometric 3D preview that updates as tiles are drawn
- Vertical height gradient selector with COLORMAP colors
- Area counter showing total and walkable tile counts
- Zoom controls (+/-) on the 2D canvas
- Simplified single-row toolbar
- Wall height control in the preview panel

Co-Authored-By: medievalshell <medievalshell@users.noreply.github.com>
This commit is contained in:
simoleo89
2026-03-16 22:09:52 +01:00
parent ae4ecc42f0
commit d6fbd19ee0
6 changed files with 647 additions and 257 deletions
@@ -1,45 +1,32 @@
import { FC, useState } from 'react';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { FC } from 'react';
import { LocalizeText } from '../../../api';
import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common';
import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer';
import { Flex, LayoutGridItem, Text } from '../../../common';
import { FloorAction } from '@nitrots/nitro-renderer';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const MIN_WALL_HEIGHT: number = 0;
const MAX_WALL_HEIGHT: number = 16;
const MIN_FLOOR_HEIGHT: number = 0;
const MAX_FLOOR_HEIGHT: number = 26;
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
interface FloorplanOptionsViewProps
{
onCanvasScroll?(direction: ScrollDirection): void;
}
export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
{
const { onCanvasScroll = () => {} } = props;
const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
const [ floorHeight, setFloorHeight ] = useState(0);
const [ isSquareSelectMode, setSquareSelectMode ] = useState(false);
const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext();
const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode;
const selectAction = (action: number) =>
{
setFloorAction(action);
FloorplanEditor.instance.actionSettings.currentAction = action;
}
};
const toggleSquareSelectMode = () =>
{
const nextValue = FloorplanEditor.instance.toggleSquareSelectMode();
setSquareSelectMode(nextValue);
}
FloorplanEditor.instance.toggleSquareSelectMode();
// force re-render by toggling action to same value
setFloorAction(prev => prev);
};
const changeDoorDirection = () =>
{
@@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
}
};
const onFloorHeightChange = (value: number) =>
const onWallThicknessChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = 0;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
if(value > 26) value = 26;
newValue.thicknessWall = value;
setFloorHeight(value);
FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36);
}
return newValue;
});
};
const onFloorThicknessChange = (value: number) =>
{
@@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
}
const onWallThicknessChange = (value: number) =>
{
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.thicknessWall = value;
return newValue;
});
}
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);
}
};
return (
<Column>
<Flex gap={ 1 }>
<Column size={ 5 } gap={ 1 }>
<Text bold>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 3 }>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ event => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ event => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
</Flex>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ event => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ event => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
</Flex>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ event => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ event => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
</Column>
<Column alignItems="center" size={ 4 }>
<Text bold>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
</Column>
<Column size={ 3 }>
<Text bold>{ LocalizeText('floor.editor.wall.height') }</Text>
<Flex alignItems="center" gap={ 1 }>
<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>
</Column>
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.room.options') }</Text>
<Flex className="align-items-center">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Column>
<Flex gap={ 2 } alignItems="center">
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ () => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ () => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ () => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ () => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ () => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ () => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
</Flex>
<Flex gap={ 2 } alignItems="center" justifyContent="between">
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.tile.height') }: { floorHeight }</Text>
<div style={ { width: '100%', maxWidth: 240 } }>
<Slider
min={ MIN_FLOOR_HEIGHT }
max={ MAX_FLOOR_HEIGHT }
step={ 1 }
value={ floorHeight }
onChange={ event => onFloorHeightChange(event) }
renderThumb={ (props, state) =>
{
const { key, style, ...rest } = (props as Record<string, any>);
return <div key={ key } style={ { backgroundColor: `#${ COLORMAP[state.valueNow.toString(33)] }`, ...style } } { ...rest }>{ state.valueNow }</div>;
} } />
</div>
</Column>
<Column gap={ 1 }>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('up') }>
<FaArrowUp className="fa-icon" />
</Button>
</Flex>
<Flex alignItems="center" justifyContent="center" gap={ 1 }>
<Button shrink onClick={ event => onCanvasScroll('left') }>
<FaArrowLeft className="fa-icon" />
</Button>
<div style={ { width: 28 } } />
<Button shrink onClick={ event => onCanvasScroll('right') }>
<FaArrowRight className="fa-icon" />
</Button>
</Flex>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('down') }>
<FaArrowDown className="fa-icon" />
</Button>
</Flex>
</Column>
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
</Flex>
</Column>
<Flex gap={ 1 } alignItems="center" className="ml-auto">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Flex>
);
}
};