diff --git a/src/components/floorplan-editor/common/FloorplanEditor.ts b/src/components/floorplan-editor/common/FloorplanEditor.ts
index 5adc2c7..f12de14 100644
--- a/src/components/floorplan-editor/common/FloorplanEditor.ts
+++ b/src/components/floorplan-editor/common/FloorplanEditor.ts
@@ -3,15 +3,18 @@ import { ActionSettings } from './ActionSettings';
import { FloorAction, HEIGHT_SCHEME, MAX_NUM_TILE_PER_AXIS, TILE_SIZE } from './Constants';
import { imageBase64, spritesheet } from './FloorplanResource';
import { Tile } from './Tile';
-import { getScreenPositionForTile } from './Utils';
+import { getScreenPositionForTile, getTileFromScreenPosition } from './Utils';
-export class FloorplanEditor
-{
+export class FloorplanEditor {
private static _INSTANCE: FloorplanEditor = null;
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 _selectionStart: NitroPoint = null;
+ private _selectionEnd: NitroPoint = null;
+
private _tilemap: Tile[][];
private _width: number;
private _height: number;
@@ -23,22 +26,20 @@ export class FloorplanEditor
private _image: HTMLImageElement;
- constructor()
- {
+ constructor() {
const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20;
const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100;
const canvas = document.createElement('canvas');
-
canvas.height = height;
canvas.width = width;
-
canvas.style.touchAction = 'none';
+ canvas.oncontextmenu = (e) => { e.preventDefault(); };
+
this._renderer = canvas.getContext('2d');
this._image = new Image();
-
this._image.src = imageBase64;
this._tilemap = [];
@@ -50,62 +51,75 @@ export class FloorplanEditor
this._actionSettings = new ActionSettings();
}
- public onPointerRelease(): void
- {
- this._isPointerDown = false;
+ public setSquareSelectMode(enabled: boolean): void {
+ this._squareSelectMode = enabled;
+ if (!enabled) {
+ this._selectionStart = null;
+ this._selectionEnd = null;
+ }
+ }
+ public get squareSelectMode(): boolean {
+ return this._squareSelectMode;
}
- public onPointerDown(event: PointerEvent): void
- {
- if(event.button === 2) return;
+ public onPointerRelease(): void {
+ this._isPointerDown = false;
+ if (this._squareSelectMode && this._selectionStart) {
+ this.finalizeSquareSelection();
+ }
+ }
+ public onPointerDown(event: PointerEvent): void {
+ if (this._squareSelectMode) {
+ event.preventDefault();
+ const location = new NitroPoint(event.offsetX, event.offsetY);
+ const [tileX, tileY] = getTileFromScreenPosition(location.x, location.y);
+ const roundedX = Math.floor(tileX);
+ const roundedY = Math.floor(tileY);
+ this._selectionStart = new NitroPoint(roundedX, roundedY);
+ this._selectionEnd = new NitroPoint(roundedX, roundedY);
+ this._isPointerDown = true;
+ return;
+ }
+ if (event.button === 2) return;
const location = new NitroPoint(event.offsetX, event.offsetY);
-
this._isPointerDown = true;
-
this.tileHitDetection(location, true);
}
- public onPointerMove(event: PointerEvent): void
- {
- if(!this._isPointerDown) return;
-
+ public onPointerMove(event: PointerEvent): void {
+ if (!this._isPointerDown) return;
+ 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);
}
- private tileHitDetection(tempPoint: NitroPoint, isClick: boolean = false): boolean
- {
+ private tileHitDetection(tempPoint: NitroPoint, isClick: boolean = false): boolean {
const mousePositionX = Math.floor(tempPoint.x);
const mousePositionY = Math.floor(tempPoint.y);
-
const width = TILE_SIZE;
const height = TILE_SIZE / 2;
-
- for(let y = 0; y < this._tilemap.length; y++)
- {
- for(let x = 0; x < this.tilemap[y].length; x++)
- {
- const [ tileStartX, tileStartY ] = getScreenPositionForTile(x, y);
-
+ for (let y = 0; y < this._tilemap.length; y++) {
+ for (let x = 0; x < this.tilemap[y].length; x++) {
+ const [tileStartX, tileStartY] = getScreenPositionForTile(x, y);
const centreX = tileStartX + (width / 2);
const centreY = tileStartY + (height / 2);
-
const dx = Math.abs(mousePositionX - centreX);
const dy = Math.abs(mousePositionY - centreY);
-
- const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1); //todo: improve this
-
- if(solution)
- {
- if(this._isPointerDown)
- {
- if(isClick)
- {
+ const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1);
+ if (solution) {
+ if (this._isPointerDown) {
+ if (isClick) {
this.onClick(x, y);
- }
- else if(this._lastUsedTile.x !== x || this._lastUsedTile.y !== y)
- {
+ } else if (this._lastUsedTile.x !== x || this._lastUsedTile.y !== y) {
this._lastUsedTile.x = x;
this._lastUsedTile.y = y;
this.onClick(x, y);
@@ -118,21 +132,16 @@ export class FloorplanEditor
return false;
}
- private onClick(x: number, y: number, render: boolean = true, force: boolean = false): void
- {
+ private onClick(x: number, y: number, render: boolean = true, force: boolean = false): void {
const tile = this._tilemap[y][x];
- // When forcing, treat background ('x') as index 0.
let currentHeightIndex = (tile.height === 'x' && force) ? 0 : HEIGHT_SCHEME.indexOf(tile.height);
let futureHeightIndex = 0;
-
- switch(this._actionSettings.currentAction)
- {
+ switch (this._actionSettings.currentAction) {
case FloorAction.DOOR:
- if (!force && tile.height !== 'x')
- {
+ if (!force && tile.height !== 'x') {
this._doorLocation.x = x;
this._doorLocation.y = y;
- if(render) this.renderTiles();
+ if (render) this.renderTiles();
}
return;
case FloorAction.UP:
@@ -150,58 +159,51 @@ export class FloorplanEditor
futureHeightIndex = 0;
break;
}
-
- if(futureHeightIndex === -1) return;
- if(currentHeightIndex === futureHeightIndex) return;
-
- // Only update _width and _height if not forcing.
- if (!force && futureHeightIndex > 0)
- {
+ if (futureHeightIndex === -1) return;
+ if (currentHeightIndex === futureHeightIndex) return;
+ if (!force && futureHeightIndex > 0) {
if ((x + 1) > this._width) this._width = x + 1;
if ((y + 1) > this._height) this._height = y + 1;
}
-
const newHeight = HEIGHT_SCHEME[futureHeightIndex];
if (!newHeight) return;
-
this._tilemap[y][x].height = newHeight;
-
- if(render) this.renderTiles();
+ if (render) this.renderTiles();
}
- public renderTiles(): void
- {
+ public renderTiles(): void {
this.clearCanvas();
-
- for(let y = 0; y < this._tilemap.length; y++)
- {
- for(let x = 0; x < this.tilemap[y].length; x++)
- {
+ 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];
let assetName = tile.height;
-
- if(this._doorLocation.x === x && this._doorLocation.y === y)
+ if (this._doorLocation.x === x && this._doorLocation.y === y)
assetName = FloorplanEditor.TILE_DOOR;
-
- if(tile.isBlocked) assetName = FloorplanEditor.TILE_BLOCKED;
-
+ if (tile.isBlocked) assetName = FloorplanEditor.TILE_BLOCKED;
if ((tile.height === 'x' || tile.height === 'X') && tile.isBlocked) assetName = 'x';
-
- const [ positionX, positionY ] = getScreenPositionForTile(x, y);
-
+ const [positionX, positionY] = getScreenPositionForTile(x, y);
const asset = spritesheet.frames[assetName];
-
- if (asset === undefined)
- {
- console.warn(`Asset "${ assetName }" not found in spritesheet.`);
+ if (asset === undefined) {
+ 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);
-
- // If the tile is selected, draw a semi-transparent blue overlay.
- if (tile.selected)
- {
+ 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.
+ 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);
+ const selMinY = Math.min(this._selectionStart.y, this._selectionEnd.y);
+ const selMaxY = Math.max(this._selectionStart.y, this._selectionEnd.y);
+ if (x >= selMinX && x <= selMaxX && y >= selMinY && y <= selMaxY) {
+ this.renderer.fillStyle = 'rgba(0, 255, 0, 0.3)';
+ this.renderer.fillRect(positionX, positionY, asset.frame.w, asset.frame.h);
+ continue;
+ }
+ }
+
+ if (tile.selected) {
this.renderer.fillStyle = 'rgba(0, 0, 255, 0.3)';
this.renderer.fillRect(positionX, positionY, asset.frame.w, asset.frame.h);
}
@@ -209,37 +211,53 @@ export class FloorplanEditor
}
}
- 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);
+ // 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.recalcActiveArea();
+ this.renderTiles();
}
-
- this.recalcActiveArea();
- this.renderTiles();
- }
+ private finalizeSquareSelection(): void {
+ const startX = Math.floor(this._selectionStart.x);
+ const startY = Math.floor(this._selectionStart.y);
+ const endX = Math.floor(this._selectionEnd.x);
+ const endY = Math.floor(this._selectionEnd.y);
+ const minX = Math.min(startX, endX);
+ const maxX = Math.max(startX, endX);
+ const minY = Math.min(startY, endY);
+ const maxY = Math.max(startY, endY);
+ this.selectSquareField(minX, minY, maxX, maxY);
+ this._selectionStart = null;
+ this._selectionEnd = null;
+ this.renderTiles();
+ }
- // New helper method to recalculate active area dimensions.
- private recalcActiveArea(): void
- {
+ private selectSquareField(x1: number, y1: number, x2: number, y2: number): void {
+ for (let y = y1; y <= y2; y++) {
+ for (let x = x1; x <= x2; x++) {
+ if (this._tilemap[y] && this._tilemap[y][x]) {
+ this._tilemap[y][x].selected = true;
+ this.onClick(x, y, false, true);
+ }
+ }
+ }
+ this.recalcActiveArea();
+ this.renderTiles();
+ }
+
+ private recalcActiveArea(): void {
this._width = 0;
this._height = 0;
- for (let y = 0; y < this._tilemap.length; y++)
- {
- for (let x = 0; x < this._tilemap[y].length; x++)
- {
- if (this._tilemap[y][x].height !== 'x')
- {
+ for (let y = 0; y < this._tilemap.length; y++) {
+ for (let x = 0; x < this._tilemap[y].length; x++) {
+ if (this._tilemap[y][x].height !== 'x') {
if ((x + 1) > this._width) this._width = x + 1;
if ((y + 1) > this._height) this._height = y + 1;
}
@@ -247,124 +265,81 @@ export class FloorplanEditor
}
}
- public setTilemap(map: string, blockedTiles: boolean[][]): void
- {
+ public setTilemap(map: string, blockedTiles: boolean[][]): void {
this._tilemap = [];
const roomMapStringSplit = map.split('\r');
-
let width = 0;
let height = roomMapStringSplit.length;
-
- // find the map width, height
- for(let y = 0; y < height; y++)
- {
+ for (let y = 0; y < height; y++) {
const originalRow = roomMapStringSplit[y];
-
- if(originalRow.length === 0)
- {
+ if (originalRow.length === 0) {
roomMapStringSplit.splice(y, 1);
height = roomMapStringSplit.length;
y--;
continue;
}
-
- if(originalRow.length > width)
- {
+ if (originalRow.length > width) {
width = originalRow.length;
}
}
- // fill map with room heightmap tiles
- for(let y = 0; y < height; y++)
- {
+ for (let y = 0; y < height; y++) {
this._tilemap[y] = [];
const rowString = roomMapStringSplit[y];
-
- for(let x = 0; x < width; x++)
- {
+ for (let x = 0; x < width; x++) {
const blocked = (blockedTiles[y] && blockedTiles[y][x]) || false;
-
const char = rowString[x];
- if(((!(char === 'x')) && (!(char === 'X')) && char))
- {
+ if ((!(char === 'x')) && (!(char === 'X')) && char) {
this._tilemap[y][x] = new Tile(char, blocked);
- }
- else
- {
+ } else {
this._tilemap[y][x] = new Tile('x', blocked);
}
}
-
- for(let x = width; x < MAX_NUM_TILE_PER_AXIS; x++)
- {
+ for (let x = width; x < MAX_NUM_TILE_PER_AXIS; x++) {
this.tilemap[y][x] = new Tile('x', false);
}
}
-
- // fill remaining map with empty tiles
- for(let y = height; y < MAX_NUM_TILE_PER_AXIS; y++)
- {
- if(!this.tilemap[y]) this.tilemap[y] = [];
- for(let x = 0; x < MAX_NUM_TILE_PER_AXIS; x++)
- {
+ for (let y = height; y < MAX_NUM_TILE_PER_AXIS; y++) {
+ if (!this.tilemap[y]) this.tilemap[y] = [];
+ for (let x = 0; x < MAX_NUM_TILE_PER_AXIS; x++) {
this.tilemap[y][x] = new Tile('x', false);
}
}
-
this._width = width;
this._height = height;
}
- public getCurrentTilemapString(): string
- {
+ public getCurrentTilemapString(): string {
const highestTile = this._tilemap[this._height - 1][this._width - 1];
-
- if(highestTile.height === 'x')
- {
+ if (highestTile.height === 'x') {
this._width = -1;
this._height = -1;
-
- for(let y = MAX_NUM_TILE_PER_AXIS - 1; y >= 0; y--)
- {
- if(!this._tilemap[y]) continue;
-
- for(let x = MAX_NUM_TILE_PER_AXIS - 1; x >= 0; x--)
- {
- if(!this._tilemap[y][x]) continue;
-
+ for (let y = MAX_NUM_TILE_PER_AXIS - 1; y >= 0; y--) {
+ if (!this._tilemap[y]) continue;
+ for (let x = MAX_NUM_TILE_PER_AXIS - 1; x >= 0; x--) {
+ if (!this._tilemap[y][x]) continue;
const tile = this._tilemap[y][x];
-
- if(tile.height !== 'x')
- {
- if((x + 1) > this._width)
+ if (tile.height !== 'x') {
+ if ((x + 1) > this._width)
this._width = x + 1;
-
- if((y + 1) > this._height)
+ if ((y + 1) > this._height)
this._height = y + 1;
}
}
}
}
-
const rows = [];
-
- for(let y = 0; y < this._height; y++)
- {
+ for (let y = 0; y < this._height; y++) {
const row = [];
-
- for(let x = 0; x < this._width; x++)
- {
+ for (let x = 0; x < this._width; x++) {
const tile = this._tilemap[y][x];
row[x] = tile.height;
}
-
rows[y] = row.join('');
}
-
return rows.join('\r');
}
- public clear(): void
- {
+ public clear(): void {
this._tilemap = [];
this._doorLocation.set(-1, -1);
this._width = 0;
@@ -375,44 +350,35 @@ export class FloorplanEditor
this.clearCanvas();
}
- public clearCanvas(): void
- {
+ public clearCanvas(): void {
this.renderer.fillStyle = '#000000';
- this.renderer.fillRect(0, 0, this._renderer.canvas.width, this._renderer.canvas.height);
+ this.renderer.fillRect(0, 0, this._renderer.canvas.width, this._renderer.canvas.height);
}
- public get renderer(): CanvasRenderingContext2D
- {
+ public get renderer(): CanvasRenderingContext2D {
return this._renderer;
}
- public get tilemap(): Tile[][]
- {
+ public get tilemap(): Tile[][] {
return this._tilemap;
}
- public get doorLocation(): NitroPoint
- {
+ public get doorLocation(): NitroPoint {
return this._doorLocation;
}
- public set doorLocation(value: NitroPoint)
- {
+ public set doorLocation(value: NitroPoint) {
this._doorLocation = value;
}
- public get actionSettings(): ActionSettings
- {
+ public get actionSettings(): ActionSettings {
return this._actionSettings;
}
- public static get instance(): FloorplanEditor
- {
- if(!FloorplanEditor._INSTANCE)
- {
+ public static get instance(): FloorplanEditor {
+ if (!FloorplanEditor._INSTANCE) {
FloorplanEditor._INSTANCE = new FloorplanEditor();
}
-
return FloorplanEditor._INSTANCE;
}
}
diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx
index 13e5e7d..556fa57 100644
--- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx
+++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx
@@ -141,6 +141,10 @@ export const FloorplanOptionsView: FC<{}> = props =>
FloorplanEditor.instance.toggleSelectAll() }>
+ {
+ FloorplanEditor.instance.setSquareSelectMode(!FloorplanEditor.instance.squareSelectMode);}
+ }>
+