diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index 1b2a3c4..eb528cf 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,25 +8,13 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; - floorHeight: number; - setFloorHeight: Dispatch>; - floorAction: number; - setFloorAction: Dispatch>; - tilemapVersion: number; - areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null, - floorHeight: 0, - setFloorHeight: null, - floorAction: 3, - setFloorAction: null, - tilemapVersion: 0, - areaInfo: { total: 0, walkable: 0 } + setVisualizationSettings: null }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 4003d13..59f7709 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,22 +1,19 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useState } from 'react'; -import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC, useEffect, useState } from 'react'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } 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, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber } 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'; -const MIN_WALL_HEIGHT = 0; -const MAX_WALL_HEIGHT = 16; + +type ScrollDirection = 'up' | 'down' | 'left' | 'right'; export const FloorplanEditorView: FC<{}> = props => { @@ -37,65 +34,7 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - 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 [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); const saveFloorChanges = () => { @@ -108,50 +47,16 @@ 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.DISPOSED, event => setIsVisible(false)); @@ -212,7 +117,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -235,42 +140,17 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - - - - - - - - { LocalizeText('floor.editor.wall.height') } - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - Area: { areaInfo.total } ({ areaInfo.walkable } caselle) - - - + + canvasScrollHandler && canvasScrollHandler(direction) } /> + - + + @@ -281,4 +161,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -}; +} diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index 9db0903..e8f39a8 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; +type ScrollDirection = 'up' | 'down' | 'left' | 'right'; + interface FloorplanCanvasViewProps extends ColumnProps { + setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, ...rest } = props; - const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; + const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); - const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); - const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,16 +63,39 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); + const onClickArrowButton = (scrollDirection: ScrollDirection) => + { + const element = elementRef.current; + + if(!element) return; + + switch(scrollDirection) + { + case 'up': + element.scrollBy({ top: -10 }); + break; + case 'down': + element.scrollBy({ top: 10 }); + break; + case 'left': + element.scrollBy({ left: -10 }); + break; + case 'right': + element.scrollBy({ left: 10 }); + break; + } + } + const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -86,10 +109,7 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - }; - - const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); - const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); + } useEffect(() => { @@ -104,15 +124,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - }; + } }); - }; + } }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -124,56 +144,45 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - const wrapper = canvasWrapperRef.current; - - if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); + + currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); + currentElement.addEventListener('pointerout', onPointerEvent); + currentElement.addEventListener('pointerdown', onPointerEvent); + currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); + currentElement.removeEventListener('pointerout', onPointerEvent); + currentElement.removeEventListener('pointerdown', onPointerEvent); + currentElement.removeEventListener('pointermove', onPointerEvent); } - }; + } }, []); + useEffect(() => + { + if(!setScrollHandler) return; + + setScrollHandler(() => onClickArrowButton); + + return () => setScrollHandler(null); + }, [ setScrollHandler ]); + return ( - - -
- -
- - -
+ + { children } ); -}; +} diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx deleted file mode 100644 index 8163c98..0000000 --- a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC } from 'react'; -import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; -import { FloorplanEditor } from '@nitrots/nitro-renderer'; -import { Column, Text } from '../../../common'; -import { useFloorplanEditorContext } from '../FloorplanEditorContext'; - -const colormap = COLORMAP as Record; - -export const FloorplanHeightSelector: FC<{}> = () => -{ - const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); - - const onSelectHeight = (height: number) => - { - setFloorHeight(height); - setFloorAction(FloorAction.SET); - - FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; - FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); - }; - - const heights: number[] = []; - - for(let i = 26; i >= 0; i--) heights.push(i); - - return ( - - { floorHeight } -
- { heights.map(h => - { - const char = HEIGHT_SCHEME[h + 1]; - const color = colormap[char] || '101010'; - const isActive = (floorHeight === h); - - return ( -
onSelectHeight(h) } - title={ `${ h }` } - /> - ); - }) } -
- - ); -}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index d4e7705..5207b15 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,32 +1,45 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText } from '../../../api'; -import { Flex, LayoutGridItem, Text } from '../../../common'; -import { FloorAction } from '@nitrots/nitro-renderer'; +import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; +import { COLORMAP, 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 = props => { - const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); - const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; - + 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 selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - }; + } const toggleSquareSelectMode = () => { - FloorplanEditor.instance.toggleSquareSelectMode(); - // force re-render by toggling action to same value - setFloorAction(prev => prev); - }; + const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); + + setSquareSelectMode(nextValue); + } const changeDoorDirection = () => { @@ -45,19 +58,18 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - }; + } - const onWallThicknessChange = (value: number) => + const onFloorHeightChange = (value: number) => { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; + if(isNaN(value) || (value <= 0)) value = 0; - newValue.thicknessWall = value; + if(value > 26) value = 26; - return newValue; - }); - }; + setFloorHeight(value); + + FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); + } const onFloorThicknessChange = (value: number) => { @@ -69,54 +81,157 @@ export const FloorplanOptionsView: FC = 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 ( - - - { LocalizeText('floor.plan.editor.draw.mode') } - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - + + + + { LocalizeText('floor.plan.editor.draw.mode') } + + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + + + + { LocalizeText('floor.plan.editor.enter.direction') } + + + + { LocalizeText('floor.editor.wall.height') } + + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + + { LocalizeText('floor.plan.editor.room.options') } + + + + + - - { LocalizeText('floor.plan.editor.enter.direction') } - + + + { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } +
+ onFloorHeightChange(event) } + renderThumb={ (props, state) => + { + const { key, style, ...rest } = (props as Record); + + return
{ state.valueNow }
; + } } /> +
+
+ + + + + + +
+ + + + + + - - - - - + ); -}; +} \ No newline at end of file diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx deleted file mode 100644 index cd82a9c..0000000 --- a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { FC, useEffect, useRef } from 'react'; -import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; -import { useFloorplanEditorContext } from '../FloorplanEditorContext'; - -const colormap = COLORMAP as Record; - -const PREVIEW_TILE_W = 16; -const PREVIEW_TILE_H = 8; -const PREVIEW_BLOCK_H = 5; -const WALL_HEIGHT_PX = 40; -const WALL_COLOR = '#6B7B5E'; -const WALL_SIDE_COLOR = '#5A6A4F'; -const WALL_TOP_COLOR = '#7D8E6F'; - -function hexToRgb(hex: string): [number, number, number] -{ - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 4), 16); - const b = parseInt(hex.substring(4, 6), 16); - - return [ r, g, b ]; -} - -function rgbToHex(r: number, g: number, b: number): string -{ - return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; -} - -function darken(hex: string, factor: number): string -{ - const [ r, g, b ] = hexToRgb(hex); - - return rgbToHex( - Math.floor(r * factor), - Math.floor(g * factor), - Math.floor(b * factor) - ); -} - -function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } -{ - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - - 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; - - if(x < minX) minX = x; - if(x > maxX) maxX = x; - if(y < minY) minY = y; - if(y > maxY) maxY = y; - } - } - - if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; - - return { minX, minY, maxX, maxY }; -} - -function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void -{ - const ctx = canvas.getContext('2d'); - const tilemap = FloorplanEditor.instance.tilemap; - - if(!ctx || !tilemap || tilemap.length === 0) - { - if(ctx) - { - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - - return; - } - - const bounds = getTilemapBounds(tilemap); - const tilesW = bounds.maxX - bounds.minX + 1; - const tilesH = bounds.maxY - bounds.minY + 1; - - // find max height for offset calculation - let maxTileHeight = 0; - - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; - - const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; - - if(hi > maxTileHeight) maxTileHeight = hi; - } - } - - // calculate isometric bounds - const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; - const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; - - // scale to fit canvas - const scaleX = (canvas.width - 20) / isoW; - const scaleY = (canvas.height - 20) / isoH; - const scale = Math.min(scaleX, scaleY, 3); - - const offsetX = (canvas.width - isoW * scale) / 2; - const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; - - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.save(); - ctx.translate(offsetX, offsetY); - ctx.scale(scale, scale); - - const tw = PREVIEW_TILE_W; - const th = PREVIEW_TILE_H; - - function isoX(gx: number, gy: number): number - { - return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; - } - - function isoY(gx: number, gy: number): number - { - return (gx - bounds.minX + gy - bounds.minY) * th; - } - - function hasActiveTile(gx: number, gy: number): boolean - { - return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; - } - - function getTileHeight(gx: number, gy: number): number - { - if(!hasActiveTile(gx, gy)) return 0; - - return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); - } - - // draw walls on north and west edges - const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; - - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!hasActiveTile(x, y)) continue; - - const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; - const cx = isoX(x, y); - const cy = isoY(x, y) - tileH; - - // west wall (no tile to the left) - if(!hasActiveTile(x - 1, y)) - { - ctx.beginPath(); - ctx.moveTo(cx, cy + th); - ctx.lineTo(cx, cy + th - wallH); - ctx.lineTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw, cy); - ctx.closePath(); - ctx.fillStyle = WALL_SIDE_COLOR; - ctx.fill(); - ctx.strokeStyle = '#4A5A3F'; - ctx.lineWidth = 0.5; - ctx.stroke(); - } - - // north wall (no tile above) - if(!hasActiveTile(x, y - 1)) - { - ctx.beginPath(); - ctx.moveTo(cx + tw, cy); - ctx.lineTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw * 2, cy + th - wallH); - ctx.lineTo(cx + tw * 2, cy + th); - ctx.closePath(); - ctx.fillStyle = WALL_COLOR; - ctx.fill(); - ctx.strokeStyle = '#4A5A3F'; - ctx.lineWidth = 0.5; - ctx.stroke(); - } - - // wall top cap - corner - if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) - { - ctx.beginPath(); - ctx.moveTo(cx + tw, cy - wallH); - ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); - ctx.lineTo(cx + tw, cy - wallH - th * 0.6); - ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); - ctx.closePath(); - ctx.fillStyle = WALL_TOP_COLOR; - ctx.fill(); - } - } - } - - // draw tiles back-to-front - for(let y = bounds.minY; y <= bounds.maxY; y++) - { - for(let x = bounds.minX; x <= bounds.maxX; x++) - { - if(!hasActiveTile(x, y)) continue; - - const tile = tilemap[y][x]; - const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; - const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; - - const cx = isoX(x, y); - const cy = isoY(x, y) - tileH; - - const heightChar = tile.height; - const baseColor = colormap[heightChar] || 'aaaaaa'; - const topColor = `#${ baseColor }`; - const leftColor = darken(baseColor, 0.65); - const rightColor = darken(baseColor, 0.80); - - // draw side faces if tile has height - const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; - - // left face (visible when no neighbor to south or neighbor is shorter) - const southH = getTileHeight(x, y + 1); - const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; - - if(leftExpose > 0) - { - ctx.beginPath(); - ctx.moveTo(cx, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); - ctx.lineTo(cx, cy + th + leftExpose); - ctx.closePath(); - ctx.fillStyle = leftColor; - ctx.fill(); - } - - // right face - const eastH = getTileHeight(x + 1, y); - const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; - - if(rightExpose > 0) - { - ctx.beginPath(); - ctx.moveTo(cx + tw * 2, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); - ctx.lineTo(cx + tw * 2, cy + th + rightExpose); - ctx.closePath(); - ctx.fillStyle = rightColor; - ctx.fill(); - } - - // top face - ctx.beginPath(); - ctx.moveTo(cx + tw, cy); - ctx.lineTo(cx + tw * 2, cy + th); - ctx.lineTo(cx + tw, cy + th * 2); - ctx.lineTo(cx, cy + th); - ctx.closePath(); - ctx.fillStyle = topColor; - ctx.fill(); - - // door indicator - const door = FloorplanEditor.instance.doorLocation; - - if(door.x === x && door.y === y) - { - ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.fill(); - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 1; - ctx.stroke(); - } - } - } - - ctx.restore(); -} - -export const FloorplanPreviewView: FC<{}> = () => -{ - const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); - const canvasRef = useRef(null); - const rafRef = useRef(0); - - useEffect(() => - { - if(!canvasRef.current) return; - - if(rafRef.current) cancelAnimationFrame(rafRef.current); - - rafRef.current = requestAnimationFrame(() => - { - const canvas = canvasRef.current; - - if(!canvas) return; - - const parent = canvas.parentElement; - - if(parent) - { - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; - } - - renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); - }); - - return () => - { - if(rafRef.current) cancelAnimationFrame(rafRef.current); - }; - }, [ tilemapVersion, visualizationSettings?.wallHeight ]); - - return ( -
- -
- ); -};