mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
|
||||
import { IFloorplanSettings } from './common/IFloorplanSettings';
|
||||
import { IVisualizationSettings } from './common/IVisualizationSettings';
|
||||
|
||||
interface IFloorplanEditorContext
|
||||
{
|
||||
originalFloorplanSettings: IFloorplanSettings;
|
||||
setOriginalFloorplanSettings: Dispatch<SetStateAction<IFloorplanSettings>>;
|
||||
visualizationSettings: IVisualizationSettings;
|
||||
setVisualizationSettings: Dispatch<SetStateAction<IVisualizationSettings>>;
|
||||
}
|
||||
|
||||
const FloorplanEditorContext = createContext<IFloorplanEditorContext>({
|
||||
originalFloorplanSettings: null,
|
||||
setOriginalFloorplanSettings: null,
|
||||
visualizationSettings: null,
|
||||
setVisualizationSettings: null
|
||||
});
|
||||
|
||||
export const FloorplanEditorContextProvider: FC<ProviderProps<IFloorplanEditorContext>> = props => <FloorplanEditorContext.Provider { ...props } />;
|
||||
|
||||
export const useFloorplanEditorContext = () => useContext(FloorplanEditorContext);
|
||||
@@ -0,0 +1,160 @@
|
||||
import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../api';
|
||||
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { useMessageEvent, useNitroEvent } from '../../hooks';
|
||||
import { FloorplanEditorContextProvider } from './FloorplanEditorContext';
|
||||
import { FloorplanEditor } from './common/FloorplanEditor';
|
||||
import { IFloorplanSettings } from './common/IFloorplanSettings';
|
||||
import { IVisualizationSettings } from './common/IVisualizationSettings';
|
||||
import { convertNumbersForSaving, convertSettingToNumber } from './common/Utils';
|
||||
import { FloorplanCanvasView } from './views/FloorplanCanvasView';
|
||||
import { FloorplanImportExportView } from './views/FloorplanImportExportView';
|
||||
import { FloorplanOptionsView } from './views/FloorplanOptionsView';
|
||||
|
||||
export const FloorplanEditorView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ importExportVisible, setImportExportVisible ] = useState(false);
|
||||
const [ originalFloorplanSettings, setOriginalFloorplanSettings ] = useState<IFloorplanSettings>({
|
||||
tilemap: '',
|
||||
reservedTiles: [],
|
||||
entryPoint: [ 0, 0 ],
|
||||
entryPointDir: 2,
|
||||
wallHeight: -1,
|
||||
thicknessWall: 1,
|
||||
thicknessFloor: 1
|
||||
});
|
||||
const [ visualizationSettings, setVisualizationSettings ] = useState<IVisualizationSettings>({
|
||||
entryPointDir: 2,
|
||||
wallHeight: -1,
|
||||
thicknessWall: 1,
|
||||
thicknessFloor: 1
|
||||
});
|
||||
|
||||
const saveFloorChanges = () =>
|
||||
{
|
||||
SendMessageComposer(new UpdateFloorPropertiesMessageComposer(
|
||||
FloorplanEditor.instance.getCurrentTilemapString(),
|
||||
FloorplanEditor.instance.doorLocation.x,
|
||||
FloorplanEditor.instance.doorLocation.y,
|
||||
visualizationSettings.entryPointDir,
|
||||
convertNumbersForSaving(visualizationSettings.thicknessWall),
|
||||
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();
|
||||
};
|
||||
|
||||
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.DISPOSED, event => setIsVisible(false));
|
||||
|
||||
useMessageEvent<FloorHeightMapEvent>(FloorHeightMapEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setOriginalFloorplanSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.tilemap = parser.model;
|
||||
newValue.wallHeight = (parser.wallHeight + 1);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setVisualizationSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.wallHeight = (parser.wallHeight + 1);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<RoomVisualizationSettingsEvent>(RoomVisualizationSettingsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setOriginalFloorplanSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.thicknessFloor = convertSettingToNumber(parser.thicknessFloor);
|
||||
newValue.thicknessWall = convertSettingToNumber(parser.thicknessWall);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setVisualizationSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.thicknessFloor = convertSettingToNumber(parser.thicknessFloor);
|
||||
newValue.thicknessWall = convertSettingToNumber(parser.thicknessWall);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length < 2) return;
|
||||
|
||||
switch(parts[1])
|
||||
{
|
||||
case 'show':
|
||||
setIsVisible(true);
|
||||
return;
|
||||
case 'hide':
|
||||
setIsVisible(false);
|
||||
return;
|
||||
case 'toggle':
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
return;
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'floor-editor/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FloorplanEditorContextProvider value={ { originalFloorplanSettings: originalFloorplanSettings, setOriginalFloorplanSettings: setOriginalFloorplanSettings, visualizationSettings: visualizationSettings, setVisualizationSettings: setVisualizationSettings } }>
|
||||
{ isVisible &&
|
||||
<NitroCardView uniqueKey="floorpan-editor" className="nitro-floorplan-editor" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('floor.plan.editor.title') } onCloseClick={ () => setIsVisible(false) } />
|
||||
<NitroCardContentView overflow="hidden">
|
||||
<FloorplanOptionsView />
|
||||
<FloorplanCanvasView overflow="hidden" />
|
||||
<Flex justifyContent="between">
|
||||
<Button onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
|
||||
<ButtonGroup>
|
||||
<Button disabled={ true }>{ LocalizeText('floor.plan.editor.preview') }</Button>
|
||||
<Button onClick={ event => setImportExportVisible(true) }>{ LocalizeText('floor.plan.editor.import.export') }</Button>
|
||||
<Button onClick={ saveFloorChanges }>{ LocalizeText('floor.plan.editor.save') }</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView> }
|
||||
{ importExportVisible &&
|
||||
<FloorplanImportExportView onCloseClick={ () => setImportExportVisible(false) } /> }
|
||||
</FloorplanEditorContextProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FloorAction, HEIGHT_SCHEME } from './Constants';
|
||||
|
||||
export class ActionSettings
|
||||
{
|
||||
private _currentAction: number;
|
||||
private _currentHeight: string;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this._currentAction = FloorAction.SET;
|
||||
this._currentHeight = HEIGHT_SCHEME[1];
|
||||
}
|
||||
|
||||
public get currentAction(): number
|
||||
{
|
||||
return this._currentAction;
|
||||
}
|
||||
|
||||
public set currentAction(value: number)
|
||||
{
|
||||
this._currentAction = value;
|
||||
}
|
||||
|
||||
public get currentHeight(): string
|
||||
{
|
||||
return this._currentHeight;
|
||||
}
|
||||
|
||||
public set currentHeight(value: string)
|
||||
{
|
||||
this._currentHeight = value;
|
||||
}
|
||||
|
||||
public clear(): void
|
||||
{
|
||||
this._currentAction = FloorAction.SET;
|
||||
this._currentHeight = HEIGHT_SCHEME[1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export const TILE_SIZE = 32;
|
||||
export const MAX_NUM_TILE_PER_AXIS = 64;
|
||||
|
||||
export const HEIGHT_SCHEME: string = 'x0123456789abcdefghijklmnopq';
|
||||
|
||||
export class FloorAction
|
||||
{
|
||||
public static readonly DOOR = 0;
|
||||
public static readonly UP = 1;
|
||||
public static readonly DOWN = 2;
|
||||
public static readonly SET = 3;
|
||||
public static readonly UNSET = 4;
|
||||
}
|
||||
|
||||
export const COLORMAP: object = {
|
||||
'x': '101010',
|
||||
'0': '0065ff',
|
||||
'1': '0091ff',
|
||||
'2': '00bcff',
|
||||
'3': '00e8ff',
|
||||
'4': '00ffea',
|
||||
'5': '00ffbf',
|
||||
'6': '00ff93',
|
||||
'7': '00ff68',
|
||||
'8': '00ff3d',
|
||||
'9': '19ff00',
|
||||
'a': '44ff00',
|
||||
'b': '70ff00',
|
||||
'c': '9bff00',
|
||||
'd': 'f2ff00',
|
||||
'e': 'ffe000',
|
||||
'f': 'ffb500',
|
||||
'g': 'ff8900',
|
||||
'h': 'ff5e00',
|
||||
'i': 'ff3200',
|
||||
'j': 'ff0700',
|
||||
'k': 'ff0023',
|
||||
'l': 'ff007a',
|
||||
'm': 'ff00a5',
|
||||
'n': 'ff00d1',
|
||||
'o': 'ff00fc',
|
||||
'p': 'd600ff',
|
||||
'q': 'aa00ff'
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const ConvertTileMapToString = (map: string) => map.replace(/\r\n|\r|\n/g, '\n').toLowerCase();
|
||||
@@ -0,0 +1,442 @@
|
||||
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, getTileFromScreenPosition } from './Utils';
|
||||
|
||||
export class FloorplanEditor {
|
||||
private static _INSTANCE: FloorplanEditor = null;
|
||||
|
||||
public static readonly TILE_BLOCKED = 'r_blocked';
|
||||
public static readonly TILE_DOOR = 'r_door';
|
||||
|
||||
private _tilemap: Tile[][];
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
private _isPointerDown: boolean;
|
||||
private _doorLocation: { x: number, y: number };
|
||||
private _lastUsedTile: { x: number, y: number };
|
||||
private _renderer: CanvasRenderingContext2D;
|
||||
private _actionSettings: ActionSettings;
|
||||
private _image: HTMLImageElement;
|
||||
private _zoomLevel: number = 1.0;
|
||||
private _squareSelectMode: boolean = false;
|
||||
private _selectionStart: { x: number, y: number } | null = null;
|
||||
private _selectionEnd: { x: number, y: number } | null = null;
|
||||
|
||||
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(); }; // Added from old code
|
||||
|
||||
this._renderer = canvas.getContext('2d')!;
|
||||
this._image = new Image();
|
||||
this._image.src = imageBase64;
|
||||
|
||||
this._tilemap = [];
|
||||
this._doorLocation = { x: 0, y: 0 };
|
||||
this._width = 0;
|
||||
this._height = 0;
|
||||
this._isPointerDown = false;
|
||||
this._lastUsedTile = { x: -1, y: -1 };
|
||||
this._actionSettings = new ActionSettings();
|
||||
}
|
||||
|
||||
public setSquareSelectMode(enabled: boolean): void {
|
||||
this._squareSelectMode = enabled;
|
||||
if (!enabled) {
|
||||
this._selectionStart = null;
|
||||
this._selectionEnd = null;
|
||||
}
|
||||
}
|
||||
|
||||
public get squareSelectMode(): boolean {
|
||||
return this._squareSelectMode;
|
||||
}
|
||||
|
||||
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 = { x: event.offsetX / this._zoomLevel, y: event.offsetY / this._zoomLevel };
|
||||
const [tileX, tileY] = getTileFromScreenPosition(location.x, location.y);
|
||||
const roundedX = Math.floor(tileX);
|
||||
const roundedY = Math.floor(tileY);
|
||||
this._selectionStart = { x: roundedX, y: roundedY };
|
||||
this._selectionEnd = { x: roundedX, y: roundedY };
|
||||
this._isPointerDown = true;
|
||||
return;
|
||||
}
|
||||
if (event.button === 2) return;
|
||||
const location = { x: event.offsetX / this._zoomLevel, y: event.offsetY / this._zoomLevel };
|
||||
this._isPointerDown = true;
|
||||
this.tileHitDetection(location, true);
|
||||
}
|
||||
|
||||
public onPointerMove(event: PointerEvent): void {
|
||||
if (!this._isPointerDown) return;
|
||||
const location = { x: event.offsetX / this._zoomLevel, y: event.offsetY / this._zoomLevel };
|
||||
if (this._squareSelectMode && this._selectionStart) {
|
||||
const [tileX, tileY] = getTileFromScreenPosition(location.x, location.y);
|
||||
this._selectionEnd!.x = Math.floor(tileX);
|
||||
this._selectionEnd!.y = Math.floor(tileY);
|
||||
this.renderTiles();
|
||||
return;
|
||||
}
|
||||
this.tileHitDetection(location, false);
|
||||
}
|
||||
|
||||
private tileHitDetection(tempPoint: { x: number, y: number }, 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);
|
||||
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);
|
||||
if (solution) {
|
||||
if (this._isPointerDown) {
|
||||
if (isClick) {
|
||||
this.onClick(x, y);
|
||||
} else if (this._lastUsedTile.x !== x || this._lastUsedTile.y !== y) {
|
||||
this._lastUsedTile.x = x;
|
||||
this._lastUsedTile.y = y;
|
||||
this.onClick(x, y);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private onClick(x: number, y: number, render: boolean = true, force: boolean = false): void { // Updated from old code
|
||||
const tile = this._tilemap[y][x];
|
||||
let currentHeightIndex = (tile.height === 'x' && force) ? 0 : HEIGHT_SCHEME.indexOf(tile.height);
|
||||
let futureHeightIndex = 0;
|
||||
switch (this._actionSettings.currentAction) {
|
||||
case FloorAction.DOOR:
|
||||
if (!force && tile.height !== 'x') {
|
||||
this._doorLocation.x = x;
|
||||
this._doorLocation.y = y;
|
||||
if (render) this.renderTiles();
|
||||
}
|
||||
return;
|
||||
case FloorAction.UP:
|
||||
if (!force && tile.height === 'x') return;
|
||||
futureHeightIndex = currentHeightIndex + 1;
|
||||
break;
|
||||
case FloorAction.DOWN:
|
||||
if (!force && (tile.height === 'x' || (currentHeightIndex <= 1))) return;
|
||||
futureHeightIndex = currentHeightIndex - 1;
|
||||
break;
|
||||
case FloorAction.SET:
|
||||
futureHeightIndex = HEIGHT_SCHEME.indexOf(this._actionSettings.currentHeight);
|
||||
break;
|
||||
case FloorAction.UNSET:
|
||||
futureHeightIndex = 0;
|
||||
break;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
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];
|
||||
let assetName = tile.height;
|
||||
if (this._doorLocation.x === x && this._doorLocation.y === y)
|
||||
assetName = FloorplanEditor.TILE_DOOR;
|
||||
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 asset = spritesheet.frames[assetName];
|
||||
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 (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 = 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();
|
||||
}
|
||||
|
||||
public toggleSelectAll(): void { // Added from old code
|
||||
for (let y = 0; y < this._tilemap.length; y++) {
|
||||
for (let x = 0; x < this._tilemap[y].length; x++) {
|
||||
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();
|
||||
this.renderTiles();
|
||||
}
|
||||
|
||||
private finalizeSquareSelection(): void { // Updated from old code
|
||||
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();
|
||||
}
|
||||
|
||||
private selectSquareField(x1: number, y1: number, x2: number, y2: number): void { // Added from old code
|
||||
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 { // Added from old code
|
||||
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') {
|
||||
if ((x + 1) > this._width) this._width = x + 1;
|
||||
if ((y + 1) > this._height) this._height = y + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setTilemap(map: string, blockedTiles: boolean[][]): void {
|
||||
this._tilemap = [];
|
||||
const roomMapStringSplit = map.split('\r');
|
||||
let width = 0;
|
||||
let height = roomMapStringSplit.length;
|
||||
for (let y = 0; y < height; y++) {
|
||||
const originalRow = roomMapStringSplit[y];
|
||||
if (originalRow.length === 0) {
|
||||
roomMapStringSplit.splice(y, 1);
|
||||
height = roomMapStringSplit.length;
|
||||
y--;
|
||||
continue;
|
||||
}
|
||||
if (originalRow.length > width) {
|
||||
width = originalRow.length;
|
||||
}
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
this._tilemap[y] = [];
|
||||
const rowString = roomMapStringSplit[y];
|
||||
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) {
|
||||
this._tilemap[y][x] = new Tile(char, blocked);
|
||||
} else {
|
||||
this._tilemap[y][x] = new Tile('x', blocked);
|
||||
}
|
||||
}
|
||||
for (let x = width; x < MAX_NUM_TILE_PER_AXIS; x++) {
|
||||
this._tilemap[y][x] = new Tile('x', false);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const highestTile = this._tilemap[this._height - 1][this._width - 1];
|
||||
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;
|
||||
const tile = this._tilemap[y][x];
|
||||
if (tile.height !== 'x') {
|
||||
if ((x + 1) > this._width)
|
||||
this._width = x + 1;
|
||||
if ((y + 1) > this._height)
|
||||
this._height = y + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const rows = [];
|
||||
for (let y = 0; y < this._height; y++) {
|
||||
const row = [];
|
||||
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 {
|
||||
this._tilemap = [];
|
||||
this._doorLocation = { x: -1, y: -1 }; // Updated from old code (no .set method)
|
||||
this._width = 0;
|
||||
this._height = 0;
|
||||
this._isPointerDown = false;
|
||||
this._lastUsedTile = { x: -1, y: -1 }; // Updated from old code (no .set method)
|
||||
this._actionSettings.clear();
|
||||
this.clearCanvas();
|
||||
}
|
||||
|
||||
public clearCanvas(): void {
|
||||
this._renderer.fillStyle = '#000000';
|
||||
this._renderer.fillRect(0, 0, this._renderer.canvas.width, this._renderer.canvas.height);
|
||||
}
|
||||
|
||||
public zoomIn(): void { // Added from old code
|
||||
this._zoomLevel = Math.min(this._zoomLevel + 0.1, 2.0);
|
||||
this.adjustCanvasSize();
|
||||
this.renderTiles();
|
||||
}
|
||||
|
||||
public zoomOut(): void { // Added from old code
|
||||
this._zoomLevel = Math.max(this._zoomLevel - 0.1, 0.5);
|
||||
this.adjustCanvasSize();
|
||||
this.renderTiles();
|
||||
}
|
||||
|
||||
private adjustCanvasSize(): void { // Added from old code
|
||||
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 { // Added from old code
|
||||
return this._zoomLevel;
|
||||
}
|
||||
|
||||
public get renderer(): CanvasRenderingContext2D {
|
||||
return this._renderer;
|
||||
}
|
||||
|
||||
public get tilemap(): Tile[][] {
|
||||
return this._tilemap;
|
||||
}
|
||||
|
||||
public get doorLocation(): { x: number, y: number } {
|
||||
return this._doorLocation;
|
||||
}
|
||||
|
||||
public set doorLocation(value: { x: number, y: number }) {
|
||||
this._doorLocation = value;
|
||||
}
|
||||
|
||||
public get actionSettings(): ActionSettings {
|
||||
return this._actionSettings;
|
||||
}
|
||||
|
||||
public static get instance(): FloorplanEditor {
|
||||
if (!FloorplanEditor._INSTANCE) {
|
||||
FloorplanEditor._INSTANCE = new FloorplanEditor();
|
||||
}
|
||||
return FloorplanEditor._INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
export const imageBase64 =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGwAAAC+CAMAAADnThrbAAAAYFBMVEUAAAAiIiIAZf8A6P8A/5MZ/wCb/wD/tQD/MgD/AHr/APxDXocAkf8A/+oA/2hE/wDy/wD/iQD/BwD/AKXWAP////8AvP8A/78A/z1w/wD/4AD/XgD/ACP/ANGqAP8QEBBSz3qJAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAABJxJREFUeNrt1tuOszoMBeDpD7QFBloKDGf6/m+51UTj1PUhN1uWOup3vSQLkSzn6+vV4fAV9X9lLIcdDv/+xWKHw/d3PPPzE8k8Rj1osceoBz3z4yiZ31HauN9R2rjfUcq451HSuOdR0rjnUcK411HcuNdR3LjXUcw4btTrOG7U6zhu1Ms4adTzOGnU8zhpVBh30Eb5cQ/fKp/5UR1+51l9mfU/Mz6NxvfMuEGMu9G49f/y8vzLz4Ikif/8qopnhiF6FhNHP9aVo2cGR71lCZAvbAXkzADE/kgQvooqhM8MCNuMCUFLtiJoZiBI5ycsvD4qFs4MLLTNElFYjJUoZAYRPAsSlV/5lcpnBhU8C6y+zPqfGZ9G43tm3CDG3Wjc+p9N/Z7PgjSN//y6jmfGMXoWU0c/1rWjZ0ZHvWUpkC9sDeTMCMT+SBG+imqEz4wI24wpQUu2JmhmJEjnpyy8PmoWzowstM1SUViMtShkRhE8C1KVX/m1ymdGFTwLrL7M+p8Zn0bje2bcIMbdaNz6n039ns+CLIv//Mslnpmm6FnMHP1YXxw9MznqLcuAfGEvQM5MQOyPDOGr6ILwmQlhmzEjaMleCJqZCNL5GQuvjwsLZyYW2maZKCzGiyhkJhE8CzKVX/kXlc9MKngWWH2Z9T8zPo3G98y4QYy70bj1P5v6PZ8Fx2P851+v8cw8R8/i0dGP9dXRM7Oj3rIjkC/sFciZGYj9cUT4KroifGZG2GY8ErRkrwTNzATp/CMLr48rC2dmFtpmR1FYjFdRyMwieBYcVX7lX1U+M6vgWWD1Zdb/zPg0Gt8z4wYx7kbj1v9s6vd8FpxO8Z/fNPHMskTP4snRj3Xj6JnFUW/ZCcgXtgFyZgFif5wQvooahM8sCNuMJ4KWbEPQzEKQzj+x8PpoWDizsNA2O4nCYmxEIbOI4FlwUvmV36h8ZlHBs8Dqy6z/mfFpNL5nxg1i3I3Grf/Z1O/5LDif4z//dotn1jV6Fs+Ofqxvjp5ZHfWWnYF8YW9AzqxA7I8zwlfRDeEzK8I245mgJXsjaGYlSOefWXh93Fg4s7LQNjuLwmK8iUJmFcGz4KzyK/+m8plVBc8Cqy+z/mfGp9H4nhk3iHE3Grf+Z1O/57Mgz+M/v23jmW2LnsXc0Y916+iZzVFvWQ7kC9sCObMBsT9yhK+iFuEzG8I2Y07Qkm0JmtkI0vk5C6+PloUzGwtts1wUFmMrCplNBM+CXOVXfqvymU0FzwKrL7P+Z8an0fieGTeIcTcat/5nU7/ns6Ao4j+/6+KZfY+excLRj3Xn6JndUW9ZAeQL2wE5swOxPwqEr6IO4TM7wjZjQdCS7Qia2QnS+QULr4+OhTM7C22zQhQWYycKmV0Ez4JC5Vd+p/KZXQXPAqsvs/5nxqfR+J4ZN4hxNxq3/mdTv+ezoCzjP7/v45n7PXoWS0c/1r2jZ+6OestKIF/YHsiZOxD7o0T4KuoRPnNH2GYsCVqyPUEzd4J0fsnC66Nn4cydhbZZKQqLsReFzF0Ez4JS5Vd+r/KZuwqeBVZfZv3PjE+j8T0zbhDjbjRu/b+0PP8DZwi9QurvbfwAAAAASUVORK5CYII=';
|
||||
|
||||
export const spritesheet = {
|
||||
frames: {
|
||||
'0': {
|
||||
frame: { x: 1, y: 1, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'1': {
|
||||
frame: { x: 37, y: 1, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'2': {
|
||||
frame: { x: 73, y: 1, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'3': {
|
||||
frame: { x: 1, y: 20, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'4': {
|
||||
frame: { x: 37, y: 20, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'5': {
|
||||
frame: { x: 73, y: 20, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'6': {
|
||||
frame: { x: 1, y: 39, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'7': {
|
||||
frame: { x: 37, y: 39, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'8': {
|
||||
frame: { x: 73, y: 39, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'9': {
|
||||
frame: { x: 1, y: 58, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'a': {
|
||||
frame: { x: 37, y: 58, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'b': {
|
||||
frame: { x: 73, y: 58, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'c': {
|
||||
frame: { x: 1, y: 77, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'd': {
|
||||
frame: { x: 37, y: 77, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'e': {
|
||||
frame: { x: 73, y: 77, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'f': {
|
||||
frame: { x: 1, y: 96, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'g': {
|
||||
frame: { x: 37, y: 96, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'h': {
|
||||
frame: { x: 73, y: 96, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'i': {
|
||||
frame: { x: 1, y: 115, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'j': {
|
||||
frame: { x: 37, y: 115, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'k': {
|
||||
frame: { x: 73, y: 115, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'l': {
|
||||
frame: { x: 1, y: 134, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'm': {
|
||||
frame: { x: 37, y: 134, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'n': {
|
||||
frame: { x: 73, y: 134, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'o': {
|
||||
frame: { x: 1, y: 153, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'p': {
|
||||
frame: { x: 37, y: 153, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'q': {
|
||||
frame: { x: 73, y: 153, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'r_blocked': {
|
||||
frame: { x: 1, y: 172, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'r_door': {
|
||||
frame: { x: 37, y: 172, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
'x': {
|
||||
frame: { x: 73, y: 172, w: 34, h: 17 },
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: { x: 0, y: 0, w: 34, h: 17 },
|
||||
sourceSize: { w: 34, h: 17 },
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IVisualizationSettings } from './IVisualizationSettings';
|
||||
|
||||
export interface IFloorplanSettings extends IVisualizationSettings
|
||||
{
|
||||
tilemap: string;
|
||||
reservedTiles: boolean[][];
|
||||
entryPoint: [ number, number ];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IVisualizationSettings
|
||||
{
|
||||
entryPointDir: number;
|
||||
wallHeight: number;
|
||||
thicknessWall: number;
|
||||
thicknessFloor: number;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export class Tile
|
||||
{
|
||||
private _height: string;
|
||||
private _isBlocked: boolean;
|
||||
|
||||
constructor(height: string, isBlocked: boolean)
|
||||
{
|
||||
this._height = height;
|
||||
this._isBlocked = isBlocked;
|
||||
}
|
||||
|
||||
public get height(): string
|
||||
{
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public set height(height: string)
|
||||
{
|
||||
this._height = height;
|
||||
}
|
||||
|
||||
public get isBlocked(): boolean
|
||||
{
|
||||
return this._isBlocked;
|
||||
}
|
||||
|
||||
public set isBlocked(val: boolean)
|
||||
{
|
||||
this._isBlocked = val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { TILE_SIZE } from './Constants';
|
||||
|
||||
export const getScreenPositionForTile = (x: number, y: number): [number , number] =>
|
||||
{
|
||||
let positionX = (x * TILE_SIZE / 2) - (y * TILE_SIZE / 2);
|
||||
const positionY = (x * TILE_SIZE / 4) + (y * TILE_SIZE / 4);
|
||||
|
||||
positionX = positionX + 1024; // center the map in the canvas
|
||||
|
||||
return [ positionX, positionY ];
|
||||
};
|
||||
|
||||
export const getTileFromScreenPosition = (x: number, y: number): [number, number] =>
|
||||
{
|
||||
const translatedX = x - 1024; // after centering translation
|
||||
|
||||
const realX = ((translatedX /(TILE_SIZE / 2)) + (y / (TILE_SIZE / 4))) / 2;
|
||||
const realY = ((y /(TILE_SIZE / 4)) - (translatedX / (TILE_SIZE / 2))) / 2;
|
||||
|
||||
return [ realX, realY ];
|
||||
};
|
||||
|
||||
export const convertNumbersForSaving = (value: number): number =>
|
||||
{
|
||||
value = parseInt(value.toString());
|
||||
switch(value)
|
||||
{
|
||||
case 0:
|
||||
return -2;
|
||||
case 1:
|
||||
return -1;
|
||||
case 3:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const convertSettingToNumber = (value: number): number =>
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 0.25:
|
||||
return 0;
|
||||
case 0.5:
|
||||
return 1;
|
||||
case 2:
|
||||
return 3;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaDotCircle, FaSearchPlus, FaSearchMinus, FaUndo } from 'react-icons/fa';
|
||||
import { SendMessageComposer } from '../../../api';
|
||||
import { Base, Button, Column, ColumnProps, Flex, Grid } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
|
||||
import { FloorplanEditor } from '../common/FloorplanEditor';
|
||||
|
||||
export const FloorplanCanvasView: FC<ColumnProps> = props =>
|
||||
{
|
||||
const { gap = 1, children = null, ...rest } = props;
|
||||
const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false);
|
||||
const [ entryTileReceived, setEntryTileReceived ] = useState(false);
|
||||
const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useMessageEvent<RoomOccupiedTilesMessageEvent>(RoomOccupiedTilesMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setOriginalFloorplanSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.reservedTiles = parser.blockedTilesMap;
|
||||
|
||||
FloorplanEditor.instance.setTilemap(newValue.tilemap, newValue.reservedTiles);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setOccupiedTilesReceived(true);
|
||||
|
||||
elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0);
|
||||
});
|
||||
|
||||
useMessageEvent<RoomEntryTileMessageEvent>(RoomEntryTileMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setOriginalFloorplanSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.entryPoint = [ parser.x, parser.y ];
|
||||
newValue.entryPointDir = parser.direction;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
setVisualizationSettings(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.entryPointDir = parser.direction;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
|
||||
FloorplanEditor.instance.doorLocation = ( parser.x, parser.y );
|
||||
|
||||
setEntryTileReceived(true);
|
||||
});
|
||||
|
||||
const onClickArrowButton = (scrollDirection: string) =>
|
||||
{
|
||||
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':
|
||||
case 'pointerup':
|
||||
FloorplanEditor.instance.onPointerRelease();
|
||||
break;
|
||||
case 'pointerdown':
|
||||
FloorplanEditor.instance.onPointerDown(event);
|
||||
break;
|
||||
case 'pointermove':
|
||||
FloorplanEditor.instance.onPointerMove(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomIn = () => {
|
||||
FloorplanEditor.instance.zoomIn();
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
FloorplanEditor.instance.zoomOut();
|
||||
};
|
||||
|
||||
const handleResetZoom = () => {
|
||||
FloorplanEditor.instance._zoomLevel = 1.0; // Reset to default zoom
|
||||
FloorplanEditor.instance.adjustCanvasSize();
|
||||
FloorplanEditor.instance.renderTiles();
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
FloorplanEditor.instance.clear();
|
||||
|
||||
setVisualizationSettings(prevValue =>
|
||||
{
|
||||
return {
|
||||
wallHeight: originalFloorplanSettings.wallHeight,
|
||||
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 ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetRoomEntryTileMessageComposer());
|
||||
SendMessageComposer(new GetOccupiedTilesMessageComposer());
|
||||
|
||||
const currentElement = elementRef.current;
|
||||
|
||||
if(!currentElement) return;
|
||||
|
||||
currentElement.appendChild(FloorplanEditor.instance.renderer.canvas);
|
||||
|
||||
currentElement.addEventListener('pointerup', onPointerEvent);
|
||||
|
||||
currentElement.addEventListener('pointerout', onPointerEvent);
|
||||
|
||||
currentElement.addEventListener('pointerdown', onPointerEvent);
|
||||
|
||||
currentElement.addEventListener('pointermove', onPointerEvent);
|
||||
|
||||
return () =>
|
||||
{
|
||||
if(currentElement)
|
||||
{
|
||||
currentElement.removeEventListener('pointerup', onPointerEvent);
|
||||
|
||||
currentElement.removeEventListener('pointerout', onPointerEvent);
|
||||
|
||||
currentElement.removeEventListener('pointerdown', onPointerEvent);
|
||||
|
||||
currentElement.removeEventListener('pointermove', onPointerEvent);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isSmallScreen = () => window.innerWidth < 768;
|
||||
|
||||
return (
|
||||
<Column gap={ gap } { ...rest }>
|
||||
<Grid overflow="hidden" gap={ 1 }>
|
||||
<Column overflow="hidden" size={ isSmallScreen() ? 10 : 12 } gap={ 1 }>
|
||||
<Flex justifyContent="left" gap={ 1 }>
|
||||
<Button shrink onClick={ handleZoomIn }>
|
||||
<FaSearchPlus className="fa-icon" />
|
||||
</Button>
|
||||
<Button shrink onClick={ handleResetZoom }>
|
||||
<FaDotCircle className="fa-icon" />
|
||||
</Button>
|
||||
<Button shrink onClick={ handleZoomOut }>
|
||||
<FaSearchMinus className="fa-icon" />
|
||||
</Button>
|
||||
</Flex>
|
||||
<Base overflow="auto" innerRef={ elementRef } />
|
||||
</Column>
|
||||
</Grid>
|
||||
{ children }
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common';
|
||||
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
|
||||
import { ConvertTileMapToString } from '../common/ConvertMapToString';
|
||||
import { convertNumbersForSaving } from '../common/Utils';
|
||||
|
||||
interface FloorplanImportExportViewProps
|
||||
{
|
||||
onCloseClick(): void;
|
||||
}
|
||||
|
||||
export const FloorplanImportExportView: FC<FloorplanImportExportViewProps> = props =>
|
||||
{
|
||||
const { onCloseClick = null } = props;
|
||||
const [ map, setMap ] = useState<string>('');
|
||||
const { originalFloorplanSettings = null } = useFloorplanEditorContext();
|
||||
|
||||
const saveFloorChanges = () =>
|
||||
{
|
||||
SendMessageComposer(new UpdateFloorPropertiesMessageComposer(
|
||||
map.split('\n').join('\r'),
|
||||
originalFloorplanSettings.entryPoint[0],
|
||||
originalFloorplanSettings.entryPoint[1],
|
||||
originalFloorplanSettings.entryPointDir,
|
||||
convertNumbersForSaving(originalFloorplanSettings.thicknessWall),
|
||||
convertNumbersForSaving(originalFloorplanSettings.thicknessFloor),
|
||||
originalFloorplanSettings.wallHeight - 1
|
||||
));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
// changed from UseMountEffect
|
||||
setMap(ConvertTileMapToString(originalFloorplanSettings.tilemap));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NitroCardView className="floorplan-import-export" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('floor.plan.editor.import.export') } onCloseClick={ onCloseClick } />
|
||||
<NitroCardContentView>
|
||||
<textarea className="h-full" value={ map } onChange={ event => setMap(event.target.value) } />
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={ event => setMap(ConvertTileMapToString(originalFloorplanSettings.tilemap)) }>
|
||||
{ LocalizeText('floor.plan.editor.revert.to.last.received.map') }
|
||||
</Button>
|
||||
<Button onClick={ saveFloorChanges }>
|
||||
{ LocalizeText('floor.plan.editor.save') }
|
||||
</Button>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { LocalizeText } from '../../../api';
|
||||
import { Column, Flex, Grid, LayoutGridItem, Text } from '../../../common';
|
||||
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
|
||||
import { COLORMAP, FloorAction } from '../common/Constants';
|
||||
import { FloorplanEditor } from '../common/FloorplanEditor';
|
||||
|
||||
const MIN_WALL_HEIGHT: number = 0;
|
||||
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 [isSquareSelectMode, setIsSquareSelectMode] = useState(false);
|
||||
|
||||
const selectAction = (action: number) => {
|
||||
setFloorAction(action);
|
||||
FloorplanEditor.instance.actionSettings.currentAction = action;
|
||||
};
|
||||
|
||||
const toggleSquareSelectMode = () => {
|
||||
setIsSquareSelectMode(prev => {
|
||||
const newValue = !prev;
|
||||
FloorplanEditor.instance.setSquareSelectMode(newValue);
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const changeDoorDirection = () => {
|
||||
setVisualizationSettings(prevValue => {
|
||||
const newValue = { ...prevValue };
|
||||
newValue.entryPointDir = newValue.entryPointDir < 7 ? newValue.entryPointDir + 1 : 0;
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
const onFloorHeightChange = (value: number) => {
|
||||
if (isNaN(value) || value <= 0) value = 0;
|
||||
if (value > MAX_FLOOR_HEIGHT) value = MAX_FLOOR_HEIGHT;
|
||||
setFloorHeight(value);
|
||||
FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36);
|
||||
};
|
||||
|
||||
const onFloorThicknessChange = (value: number) => {
|
||||
setVisualizationSettings(prevValue => ({
|
||||
...prevValue,
|
||||
thicknessFloor: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onWallThicknessChange = (value: number) => {
|
||||
setVisualizationSettings(prevValue => ({
|
||||
...prevValue,
|
||||
thicknessWall: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const onWallHeightChange = (value: number) => {
|
||||
if (isNaN(value) || value <= 0) value = MIN_WALL_HEIGHT;
|
||||
if (value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
|
||||
setVisualizationSettings(prevValue => ({
|
||||
...prevValue,
|
||||
wallHeight: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const increaseWallHeight = () => {
|
||||
onWallHeightChange(Math.min(visualizationSettings.wallHeight + 1, MAX_WALL_HEIGHT));
|
||||
};
|
||||
|
||||
const decreaseWallHeight = () => {
|
||||
onWallHeightChange(Math.max(visualizationSettings.wallHeight - 1, MIN_WALL_HEIGHT));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Grid>
|
||||
<Column gap={1} size={5}>
|
||||
<Text bold>{LocalizeText('floor.plan.editor.draw.mode')}</Text>
|
||||
<Flex gap={3}>
|
||||
<div className="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>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
<div className="flex items-center gap-1">
|
||||
<FaCaretLeft className="cursor-pointer fa-icon" onClick={decreaseWallHeight} />
|
||||
<input
|
||||
className="min-h-[calc(1.5em+.5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm quantity-input"
|
||||
type="number"
|
||||
value={visualizationSettings.wallHeight}
|
||||
onChange={event => onWallHeightChange(event.target.valueAsNumber)}
|
||||
/>
|
||||
<FaCaretRight className="cursor-pointer fa-icon" onClick={increaseWallHeight} />
|
||||
</div>
|
||||
</Column>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Column size={6}>
|
||||
<Text bold>{LocalizeText('floor.plan.editor.tile.height')}: {floorHeight}</Text>
|
||||
<ReactSlider
|
||||
className="nitro-slider"
|
||||
max={MAX_FLOOR_HEIGHT}
|
||||
min={MIN_FLOOR_HEIGHT}
|
||||
renderThumb={({ key, style, ...rest }, state) => (
|
||||
<div
|
||||
key={key} // Explicitly pass key directly
|
||||
style={{ backgroundColor: `#${COLORMAP[state.valueNow.toString(33)]}`, ...style }}
|
||||
{...rest}
|
||||
>
|
||||
{state.valueNow}
|
||||
</div>
|
||||
)}
|
||||
step={1}
|
||||
value={floorHeight}
|
||||
onChange={value => onFloorHeightChange(value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column size={6}>
|
||||
<Text bold>{LocalizeText('floor.plan.editor.room.options')}</Text>
|
||||
<Flex className="items-center">
|
||||
<select
|
||||
className="min-h-[calc(1.5em+.5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] 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="min-h-[calc(1.5em+.5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] 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>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user