🆙 FloorEditor allow tile selection - Beta 1

This will allow you to select a field of tiles.
Still need the buttons fixed, but hee it is beta 1
This commit is contained in:
DuckieTM 2025-03-06 20:00:39 +01:00
parent e493428dc9
commit 31e047e803
2 changed files with 168 additions and 198 deletions

View File

@ -3,15 +3,18 @@ import { ActionSettings } from './ActionSettings';
import { FloorAction, HEIGHT_SCHEME, MAX_NUM_TILE_PER_AXIS, TILE_SIZE } from './Constants'; import { FloorAction, HEIGHT_SCHEME, MAX_NUM_TILE_PER_AXIS, TILE_SIZE } from './Constants';
import { imageBase64, spritesheet } from './FloorplanResource'; import { imageBase64, spritesheet } from './FloorplanResource';
import { Tile } from './Tile'; import { Tile } from './Tile';
import { getScreenPositionForTile } from './Utils'; import { getScreenPositionForTile, getTileFromScreenPosition } from './Utils';
export class FloorplanEditor export class FloorplanEditor {
{
private static _INSTANCE: FloorplanEditor = null; private static _INSTANCE: FloorplanEditor = null;
public static readonly TILE_BLOCKED = 'r_blocked'; public static readonly TILE_BLOCKED = 'r_blocked';
public static readonly TILE_DOOR = 'r_door'; 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 _tilemap: Tile[][];
private _width: number; private _width: number;
private _height: number; private _height: number;
@ -23,22 +26,20 @@ export class FloorplanEditor
private _image: HTMLImageElement; private _image: HTMLImageElement;
constructor() constructor() {
{
const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20; const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20;
const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100; const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.height = height; canvas.height = height;
canvas.width = width; canvas.width = width;
canvas.style.touchAction = 'none'; canvas.style.touchAction = 'none';
canvas.oncontextmenu = (e) => { e.preventDefault(); };
this._renderer = canvas.getContext('2d'); this._renderer = canvas.getContext('2d');
this._image = new Image(); this._image = new Image();
this._image.src = imageBase64; this._image.src = imageBase64;
this._tilemap = []; this._tilemap = [];
@ -50,62 +51,75 @@ export class FloorplanEditor
this._actionSettings = new ActionSettings(); this._actionSettings = new ActionSettings();
} }
public onPointerRelease(): void public setSquareSelectMode(enabled: boolean): void {
{ this._squareSelectMode = enabled;
this._isPointerDown = false; if (!enabled) {
this._selectionStart = null;
this._selectionEnd = null;
}
}
public get squareSelectMode(): boolean {
return this._squareSelectMode;
} }
public onPointerDown(event: PointerEvent): void public onPointerRelease(): void {
{ this._isPointerDown = false;
if(event.button === 2) return; 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); const location = new NitroPoint(event.offsetX, event.offsetY);
this._isPointerDown = true; this._isPointerDown = true;
this.tileHitDetection(location, true); this.tileHitDetection(location, true);
} }
public onPointerMove(event: PointerEvent): void public onPointerMove(event: PointerEvent): void {
{ if (!this._isPointerDown) return;
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); const location = new NitroPoint(event.offsetX, event.offsetY);
this.tileHitDetection(location, false); 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 mousePositionX = Math.floor(tempPoint.x);
const mousePositionY = Math.floor(tempPoint.y); const mousePositionY = Math.floor(tempPoint.y);
const width = TILE_SIZE; const width = TILE_SIZE;
const height = TILE_SIZE / 2; const height = TILE_SIZE / 2;
for (let y = 0; y < this._tilemap.length; 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);
for(let x = 0; x < this.tilemap[y].length; x++)
{
const [ tileStartX, tileStartY ] = getScreenPositionForTile(x, y);
const centreX = tileStartX + (width / 2); const centreX = tileStartX + (width / 2);
const centreY = tileStartY + (height / 2); const centreY = tileStartY + (height / 2);
const dx = Math.abs(mousePositionX - centreX); const dx = Math.abs(mousePositionX - centreX);
const dy = Math.abs(mousePositionY - centreY); const dy = Math.abs(mousePositionY - centreY);
const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1);
const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1); //todo: improve this if (solution) {
if (this._isPointerDown) {
if(solution) if (isClick) {
{
if(this._isPointerDown)
{
if(isClick)
{
this.onClick(x, y); 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.x = x;
this._lastUsedTile.y = y; this._lastUsedTile.y = y;
this.onClick(x, y); this.onClick(x, y);
@ -118,21 +132,16 @@ export class FloorplanEditor
return false; 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]; 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 currentHeightIndex = (tile.height === 'x' && force) ? 0 : HEIGHT_SCHEME.indexOf(tile.height);
let futureHeightIndex = 0; let futureHeightIndex = 0;
switch (this._actionSettings.currentAction) {
switch(this._actionSettings.currentAction)
{
case FloorAction.DOOR: case FloorAction.DOOR:
if (!force && tile.height !== 'x') if (!force && tile.height !== 'x') {
{
this._doorLocation.x = x; this._doorLocation.x = x;
this._doorLocation.y = y; this._doorLocation.y = y;
if(render) this.renderTiles(); if (render) this.renderTiles();
} }
return; return;
case FloorAction.UP: case FloorAction.UP:
@ -150,58 +159,51 @@ export class FloorplanEditor
futureHeightIndex = 0; futureHeightIndex = 0;
break; break;
} }
if (futureHeightIndex === -1) return;
if(futureHeightIndex === -1) return; if (currentHeightIndex === futureHeightIndex) return;
if(currentHeightIndex === futureHeightIndex) return; if (!force && futureHeightIndex > 0) {
// Only update _width and _height if not forcing.
if (!force && futureHeightIndex > 0)
{
if ((x + 1) > this._width) this._width = x + 1; if ((x + 1) > this._width) this._width = x + 1;
if ((y + 1) > this._height) this._height = y + 1; if ((y + 1) > this._height) this._height = y + 1;
} }
const newHeight = HEIGHT_SCHEME[futureHeightIndex]; const newHeight = HEIGHT_SCHEME[futureHeightIndex];
if (!newHeight) return; if (!newHeight) return;
this._tilemap[y][x].height = newHeight; this._tilemap[y][x].height = newHeight;
if (render) this.renderTiles();
if(render) this.renderTiles();
} }
public renderTiles(): void public renderTiles(): void {
{
this.clearCanvas(); this.clearCanvas();
for (let y = 0; y < this._tilemap.length; y++) {
for(let y = 0; y < this._tilemap.length; y++) for (let x = 0; x < this.tilemap[y].length; x++) {
{
for(let x = 0; x < this.tilemap[y].length; x++)
{
const tile = this.tilemap[y][x]; const tile = this.tilemap[y][x];
let assetName = tile.height; 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; 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'; 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]; const asset = spritesheet.frames[assetName];
if (asset === undefined) {
if (asset === undefined) console.warn(`Asset "${assetName}" not found in spritesheet.`);
{
console.warn(`Asset "${ assetName }" not found in spritesheet.`);
continue; continue;
} }
this.renderer.drawImage(this._image, asset.frame.x, asset.frame.y, asset.frame.w, asset.frame.h,
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); positionX, positionY, asset.frame.w, asset.frame.h);
// If the tile is selected, draw a semi-transparent blue overlay. // While dragging in selection mode, overlay green on tiles within the selection region.
if (tile.selected) 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.fillStyle = 'rgba(0, 0, 255, 0.3)';
this.renderer.fillRect(positionX, positionY, asset.frame.w, asset.frame.h); this.renderer.fillRect(positionX, positionY, asset.frame.w, asset.frame.h);
} }
@ -209,37 +211,53 @@ export class FloorplanEditor
} }
} }
public toggleSelectAll(): void // Toggle select all (always selects)
{ public toggleSelectAll(): void {
const newState = true;
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;
for (let y = 0; y < this._tilemap.length; y++) this.onClick(x, y, false, true);
{ }
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 selectSquareField(x1: number, y1: number, x2: number, y2: number): void {
private recalcActiveArea(): 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._width = 0;
this._height = 0; this._height = 0;
for (let y = 0; y < this._tilemap.length; y++) for (let y = 0; y < this._tilemap.length; y++) {
{ for (let x = 0; x < this._tilemap[y].length; x++) {
for (let x = 0; x < this._tilemap[y].length; x++) if (this._tilemap[y][x].height !== 'x') {
{
if (this._tilemap[y][x].height !== 'x')
{
if ((x + 1) > this._width) this._width = x + 1; if ((x + 1) > this._width) this._width = x + 1;
if ((y + 1) > this._height) this._height = y + 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 = []; this._tilemap = [];
const roomMapStringSplit = map.split('\r'); const roomMapStringSplit = map.split('\r');
let width = 0; let width = 0;
let height = roomMapStringSplit.length; let height = roomMapStringSplit.length;
for (let y = 0; y < height; y++) {
// find the map width, height
for(let y = 0; y < height; y++)
{
const originalRow = roomMapStringSplit[y]; const originalRow = roomMapStringSplit[y];
if (originalRow.length === 0) {
if(originalRow.length === 0)
{
roomMapStringSplit.splice(y, 1); roomMapStringSplit.splice(y, 1);
height = roomMapStringSplit.length; height = roomMapStringSplit.length;
y--; y--;
continue; continue;
} }
if (originalRow.length > width) {
if(originalRow.length > width)
{
width = originalRow.length; 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] = []; this._tilemap[y] = [];
const rowString = roomMapStringSplit[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 blocked = (blockedTiles[y] && blockedTiles[y][x]) || false;
const char = rowString[x]; const char = rowString[x];
if(((!(char === 'x')) && (!(char === 'X')) && char)) if ((!(char === 'x')) && (!(char === 'X')) && char) {
{
this._tilemap[y][x] = new Tile(char, blocked); this._tilemap[y][x] = new Tile(char, blocked);
} } else {
else
{
this._tilemap[y][x] = new Tile('x', blocked); 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); this.tilemap[y][x] = new Tile('x', false);
} }
} }
for (let y = height; y < MAX_NUM_TILE_PER_AXIS; y++) {
// fill remaining map with empty tiles if (!this.tilemap[y]) this.tilemap[y] = [];
for(let y = height; y < MAX_NUM_TILE_PER_AXIS; y++) for (let x = 0; x < MAX_NUM_TILE_PER_AXIS; x++) {
{
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.tilemap[y][x] = new Tile('x', false);
} }
} }
this._width = width; this._width = width;
this._height = height; this._height = height;
} }
public getCurrentTilemapString(): string public getCurrentTilemapString(): string {
{
const highestTile = this._tilemap[this._height - 1][this._width - 1]; const highestTile = this._tilemap[this._height - 1][this._width - 1];
if (highestTile.height === 'x') {
if(highestTile.height === 'x')
{
this._width = -1; this._width = -1;
this._height = -1; this._height = -1;
for (let y = MAX_NUM_TILE_PER_AXIS - 1; y >= 0; y--) {
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]) continue; if (!this._tilemap[y][x]) 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]; const tile = this._tilemap[y][x];
if (tile.height !== 'x') {
if(tile.height !== 'x') if ((x + 1) > this._width)
{
if((x + 1) > this._width)
this._width = x + 1; this._width = x + 1;
if ((y + 1) > this._height)
if((y + 1) > this._height)
this._height = y + 1; this._height = y + 1;
} }
} }
} }
} }
const rows = []; const rows = [];
for (let y = 0; y < this._height; y++) {
for(let y = 0; y < this._height; y++)
{
const row = []; const row = [];
for (let x = 0; x < this._width; x++) {
for(let x = 0; x < this._width; x++)
{
const tile = this._tilemap[y][x]; const tile = this._tilemap[y][x];
row[x] = tile.height; row[x] = tile.height;
} }
rows[y] = row.join(''); rows[y] = row.join('');
} }
return rows.join('\r'); return rows.join('\r');
} }
public clear(): void public clear(): void {
{
this._tilemap = []; this._tilemap = [];
this._doorLocation.set(-1, -1); this._doorLocation.set(-1, -1);
this._width = 0; this._width = 0;
@ -375,44 +350,35 @@ export class FloorplanEditor
this.clearCanvas(); this.clearCanvas();
} }
public clearCanvas(): void public clearCanvas(): void {
{
this.renderer.fillStyle = '#000000'; 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; return this._renderer;
} }
public get tilemap(): Tile[][] public get tilemap(): Tile[][] {
{
return this._tilemap; return this._tilemap;
} }
public get doorLocation(): NitroPoint public get doorLocation(): NitroPoint {
{
return this._doorLocation; return this._doorLocation;
} }
public set doorLocation(value: NitroPoint) public set doorLocation(value: NitroPoint) {
{
this._doorLocation = value; this._doorLocation = value;
} }
public get actionSettings(): ActionSettings public get actionSettings(): ActionSettings {
{
return this._actionSettings; return this._actionSettings;
} }
public static get instance(): FloorplanEditor public static get instance(): FloorplanEditor {
{ if (!FloorplanEditor._INSTANCE) {
if(!FloorplanEditor._INSTANCE)
{
FloorplanEditor._INSTANCE = new FloorplanEditor(); FloorplanEditor._INSTANCE = new FloorplanEditor();
} }
return FloorplanEditor._INSTANCE; return FloorplanEditor._INSTANCE;
} }
} }

View File

@ -141,6 +141,10 @@ export const FloorplanOptionsView: FC<{}> = props =>
<LayoutGridItem onClick={ event => FloorplanEditor.instance.toggleSelectAll() }> <LayoutGridItem onClick={ event => FloorplanEditor.instance.toggleSelectAll() }>
<i className="icon icon-set-select" /> <i className="icon icon-set-select" />
</LayoutGridItem> </LayoutGridItem>
<LayoutGridItem itemActive={ FloorplanEditor.instance.squareSelectMode } onClick={ event => {
FloorplanEditor.instance.setSquareSelectMode(!FloorplanEditor.instance.squareSelectMode);}
}><i className="icon icon-set-select" />
</LayoutGridItem>
</Flex> </Flex>
</Column> </Column>
<Column alignItems="center" size={ 4 }> <Column alignItems="center" size={ 4 }>