From 680eb4dd88b76c8d21a648bfc6a3abe978a3c499 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 7 Mar 2025 08:44:01 +0100 Subject: [PATCH] :up: FloorEditor allow tile selection - Beta 2 --- .../common/FloorplanEditor.ts | 99 ++++-- .../views/FloorplanCanvasView.tsx | 22 +- .../views/FloorplanOptionsView.tsx | 304 ++++++++++-------- 3 files changed, 264 insertions(+), 161 deletions(-) diff --git a/src/components/floorplan-editor/common/FloorplanEditor.ts b/src/components/floorplan-editor/common/FloorplanEditor.ts index f12de14..b14add6 100644 --- a/src/components/floorplan-editor/common/FloorplanEditor.ts +++ b/src/components/floorplan-editor/common/FloorplanEditor.ts @@ -11,7 +11,7 @@ export class FloorplanEditor { public static readonly TILE_BLOCKED = 'r_blocked'; public static readonly TILE_DOOR = 'r_door'; - private _squareSelectMode: boolean = false; // Mode flag (name remains for compatibility) + private _squareSelectMode: boolean = false; private _selectionStart: NitroPoint = null; private _selectionEnd: NitroPoint = null; @@ -23,9 +23,10 @@ export class FloorplanEditor { private _lastUsedTile: NitroPoint; private _renderer: CanvasRenderingContext2D; private _actionSettings: ActionSettings; - private _image: HTMLImageElement; + private _zoomLevel: number = 1.0; + constructor() { const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20; const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100; @@ -58,6 +59,7 @@ export class FloorplanEditor { this._selectionEnd = null; } } + public get squareSelectMode(): boolean { return this._squareSelectMode; } @@ -72,7 +74,7 @@ export class FloorplanEditor { public onPointerDown(event: PointerEvent): void { if (this._squareSelectMode) { event.preventDefault(); - const location = new NitroPoint(event.offsetX, event.offsetY); + const location = new NitroPoint(event.offsetX / this._zoomLevel, event.offsetY / this._zoomLevel); const [tileX, tileY] = getTileFromScreenPosition(location.x, location.y); const roundedX = Math.floor(tileX); const roundedY = Math.floor(tileY); @@ -82,23 +84,21 @@ export class FloorplanEditor { return; } if (event.button === 2) return; - const location = new NitroPoint(event.offsetX, event.offsetY); + const location = new NitroPoint(event.offsetX / this._zoomLevel, event.offsetY / this._zoomLevel); this._isPointerDown = true; this.tileHitDetection(location, true); } public onPointerMove(event: PointerEvent): void { if (!this._isPointerDown) return; + const location = new NitroPoint(event.offsetX / this._zoomLevel, event.offsetY / this._zoomLevel); if (this._squareSelectMode && this._selectionStart) { - const location = new NitroPoint(event.offsetX, event.offsetY); const [tileX, tileY] = getTileFromScreenPosition(location.x, location.y); this._selectionEnd.x = Math.floor(tileX); this._selectionEnd.y = Math.floor(tileY); this.renderTiles(); - // Optionally, you could add a temporary overlay here if desired. return; } - const location = new NitroPoint(event.offsetX, event.offsetY); this.tileHitDetection(location, false); } @@ -173,6 +173,9 @@ export class FloorplanEditor { public renderTiles(): void { this.clearCanvas(); + this._renderer.save(); + this._renderer.scale(this._zoomLevel, this._zoomLevel); + for (let y = 0; y < this._tilemap.length; y++) { for (let x = 0; x < this.tilemap[y].length; x++) { const tile = this.tilemap[y][x]; @@ -187,10 +190,18 @@ export class FloorplanEditor { console.warn(`Asset "${assetName}" not found in spritesheet.`); continue; } - this.renderer.drawImage(this._image, asset.frame.x, asset.frame.y, asset.frame.w, asset.frame.h, - positionX, positionY, asset.frame.w, asset.frame.h); - - // While dragging in selection mode, overlay green on tiles within the selection region. + this.renderer.drawImage( + this._image, + asset.frame.x, + asset.frame.y, + asset.frame.w, + asset.frame.h, + positionX, + positionY, + asset.frame.w, + asset.frame.h + ); + if (this._squareSelectMode && this._isPointerDown && this._selectionStart && this._selectionEnd) { const selMinX = Math.min(this._selectionStart.x, this._selectionEnd.x); const selMaxX = Math.max(this._selectionStart.x, this._selectionEnd.x); @@ -202,22 +213,51 @@ export class FloorplanEditor { continue; } } - + if (tile.selected) { - this.renderer.fillStyle = 'rgba(0, 0, 255, 0.3)'; + this.renderer.fillStyle = tile.isBlocked ? 'rgb(128, 0, 128)' : 'rgba(0, 0, 255, 0.3)'; this.renderer.fillRect(positionX, positionY, asset.frame.w, asset.frame.h); } } } + this._renderer.restore(); } - // Toggle select all (always selects) public toggleSelectAll(): void { - const newState = true; for (let y = 0; y < this._tilemap.length; y++) { for (let x = 0; x < this._tilemap[y].length; x++) { - this._tilemap[y][x].selected = newState; - this.onClick(x, y, false, true); + this._tilemap[y][x].selected = true; + if (this._actionSettings.currentAction !== FloorAction.DOOR) { + const tile = this._tilemap[y][x]; + let currentHeightIndex = tile.height === 'x' ? 0 : HEIGHT_SCHEME.indexOf(tile.height); + let futureHeightIndex = 0; + switch (this._actionSettings.currentAction) { + case FloorAction.UP: + if (tile.height === 'x') continue; + futureHeightIndex = currentHeightIndex + 1; + break; + case FloorAction.DOWN: + if (tile.height === 'x' || currentHeightIndex <= 1) continue; + futureHeightIndex = currentHeightIndex - 1; + break; + case FloorAction.SET: + futureHeightIndex = HEIGHT_SCHEME.indexOf(this._actionSettings.currentHeight); + break; + case FloorAction.UNSET: + futureHeightIndex = 0; + break; + default: + continue; + } + if (futureHeightIndex !== -1 && currentHeightIndex !== futureHeightIndex) { + const newHeight = HEIGHT_SCHEME[futureHeightIndex]; + if (newHeight) { + this._tilemap[y][x].height = newHeight; + if ((x + 1) > this._width) this._width = x + 1; + if ((y + 1) > this._height) this._height = y + 1; + } + } + } } } this.recalcActiveArea(); @@ -355,6 +395,29 @@ export class FloorplanEditor { this.renderer.fillRect(0, 0, this._renderer.canvas.width, this._renderer.canvas.height); } + public zoomIn(): void { + this._zoomLevel = Math.min(this._zoomLevel + 0.1, 2.0); + this.adjustCanvasSize(); + this.renderTiles(); + } + + public zoomOut(): void { + this._zoomLevel = Math.max(this._zoomLevel - 0.1, 0.5); + this.adjustCanvasSize(); + this.renderTiles(); + } + + private adjustCanvasSize(): void { + const baseWidth = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20; + const baseHeight = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100; + this._renderer.canvas.width = baseWidth * this._zoomLevel; + this._renderer.canvas.height = baseHeight * this._zoomLevel; + } + + public get zoomLevel(): number { + return this._zoomLevel; + } + public get renderer(): CanvasRenderingContext2D { return this._renderer; } @@ -381,4 +444,4 @@ export class FloorplanEditor { } return FloorplanEditor._INSTANCE; } -} +} \ No newline at end of file diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index 1073f01..47d4b61 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,6 +1,6 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, NitroPoint, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; -import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa'; +import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaSearchPlus, FaSearchMinus } from 'react-icons/fa'; // Added FaSearchPlus and FaSearchMinus for zoom icons import { SendMessageComposer } from '../../../api'; import { Base, Button, Column, ColumnProps, Flex, Grid } from '../../../common'; import { useMessageEvent } from '../../../hooks'; @@ -105,6 +105,14 @@ export const FloorplanCanvasView: FC = props => } } + const handleZoomIn = () => { + FloorplanEditor.instance.zoomIn(); + }; + + const handleZoomOut = () => { + FloorplanEditor.instance.zoomOut(); + }; + useEffect(() => { return () => @@ -174,9 +182,15 @@ export const FloorplanCanvasView: FC = props => - - - + + diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index ab27316..bb59d37 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -12,155 +12,181 @@ const MAX_WALL_HEIGHT: number = 16; const MIN_FLOOR_HEIGHT: number = 0; const MAX_FLOOR_HEIGHT: number = 26; -export const FloorplanOptionsView: FC<{}> = props => { - const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ floorHeight, setFloorHeight ] = useState(0); - - const selectAction = (action: number) => { - setFloorAction(action); - FloorplanEditor.instance.actionSettings.currentAction = action; - } +export const FloorplanOptionsView: FC<{}> = props => +{ + const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ floorHeight, setFloorHeight ] = useState(0); + + const selectAction = (action: number) => + { + setFloorAction(action); - const changeDoorDirection = () => { - setVisualizationSettings(prevValue => { - const newValue = { ...prevValue }; - if(newValue.entryPointDir < 7) { - ++newValue.entryPointDir; - } else { - newValue.entryPointDir = 0; - } - return newValue; - }); - } + FloorplanEditor.instance.actionSettings.currentAction = action; + } - const onFloorHeightChange = (value: number) => { - if(isNaN(value) || (value <= 0)) value = 0; - if(value > 26) value = 26; - setFloorHeight(value); - FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); - } + const changeDoorDirection = () => + { + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; - const onFloorThicknessChange = (value: number) => { - setVisualizationSettings(prevValue => { - const newValue = { ...prevValue }; - newValue.thicknessFloor = value; - return newValue; - }); - } + if(newValue.entryPointDir < 7) + { + ++newValue.entryPointDir; + } + else + { + newValue.entryPointDir = 0; + } - const onWallThicknessChange = (value: number) => { - setVisualizationSettings(prevValue => { - const newValue = { ...prevValue }; - newValue.thicknessWall = value; - return newValue; - }); - } + 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 onFloorHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = 0; - const increaseWallHeight = () => { - let height = (visualizationSettings.wallHeight + 1); - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - onWallHeightChange(height); - } + if(value > 26) value = 26; - const decreaseWallHeight = () => { - let height = (visualizationSettings.wallHeight - 1); - if(height <= 0) height = MIN_WALL_HEIGHT; - onWallHeightChange(height); - } + setFloorHeight(value); - return ( - - - - { LocalizeText('floor.plan.editor.draw.mode') } - + FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); + } + + const onFloorThicknessChange = (value: number) => + { + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.thicknessFloor = value; + + 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 ( + - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - + + { LocalizeText('floor.plan.editor.draw.mode') } + + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + { FloorplanEditor.instance.setSquareSelectMode(!FloorplanEditor.instance.squareSelectMode);} }> + + + + + + { LocalizeText('floor.plan.editor.enter.direction') } + + + + { LocalizeText('floor.editor.wall.height') } + + + onWallHeightChange(event.target.valueAsNumber) } /> + + + - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - + + { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } + onFloorHeightChange(event) } + renderThumb={ ({ style, ...rest }, state) =>
{ state.valueNow }
} /> +
+ + { LocalizeText('floor.plan.editor.room.options') } + + + + +
- selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - { - FloorplanEditor.instance.setSquareSelectMode(!FloorplanEditor.instance.squareSelectMode); - } }> - - -
- - { LocalizeText('floor.plan.editor.enter.direction') } - - - - { LocalizeText('floor.editor.wall.height') } - - - onWallHeightChange(event.target.valueAsNumber) } /> - - - -
- - - { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } - onFloorHeightChange(event) } - renderThumb={ ({ style, key, ...thumbProps }, state) => ( -
- { state.valueNow } -
- ) } - /> -
- - { LocalizeText('floor.plan.editor.room.options') } - - - - - -
-
- ); -} + ); +} \ No newline at end of file