From c8cc628ef39acc818b67d31567ab015161bab4eb Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Wed, 3 Jan 2024 22:37:57 +1300 Subject: [PATCH 1/7] Tile class for various world space conversions --- src/mapeditor/Tiles.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/mapeditor/Tiles.ts diff --git a/src/mapeditor/Tiles.ts b/src/mapeditor/Tiles.ts new file mode 100644 index 0000000..fd667cf --- /dev/null +++ b/src/mapeditor/Tiles.ts @@ -0,0 +1,41 @@ +import {getMapCoordinates, getMapSquareId} from "../rs/map/MapFileIndex"; + +const borderSize = 6; + +export const getMapAndTile = (mapId: number, tileId: number): number => { + const mapCoordinates = getMapCoordinates(mapId); + + const mapX = mapCoordinates.mapX & 0xFFFF; + const mapY = mapCoordinates.mapY & 0xFFFF; + + const packedTileId = + (BigInt(mapX) << 48n) | (BigInt(mapY) << 32n) | (BigInt(tileId >> 8) << 16n) | BigInt(tileId & 0xFF); + + return Number(packedTileId); +}; + + + +export const getWorldTileId = (worldX: number, worldY: number): number => { + const mapX = Math.floor(worldX / 64); + const mapY = Math.floor(worldY / 64); + const tileX = (worldX % 64) + borderSize; + const tileY = (worldY % 64) + borderSize; + + const packedTileId = + (BigInt(mapX & 0xFFFF) << 48n) | (BigInt(mapY & 0xFFFF) << 32n) | (BigInt(tileX & 0xFFFF) << 16n) | BigInt(tileY & 0xFFFF); + + return Number(packedTileId); +}; + +export const getWorldTileIdFromLocal = (mapId: number, tileX: number, tileY: number): bigint => { + const mapCoordinates = getMapCoordinates(mapId); + + const mapX = BigInt(mapCoordinates.mapX) & 0xFFFFn; + const mapY = BigInt(mapCoordinates.mapY) & 0xFFFFn; + const shiftedMapX = mapX << 32n; + const shiftedMapY = mapY << 16n; + + return shiftedMapX | shiftedMapY | BigInt(tileX & 0xFFFF) << 16n | BigInt(tileY & 0xFFFF); +}; + From 2a25355c81779d2048da6827e9980635d7cc278f Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Wed, 3 Jan 2024 22:39:23 +1300 Subject: [PATCH 2/7] Undo/redo manager --- src/mapeditor/UndoRedoManager.ts | 129 +++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/mapeditor/UndoRedoManager.ts diff --git a/src/mapeditor/UndoRedoManager.ts b/src/mapeditor/UndoRedoManager.ts new file mode 100644 index 0000000..9d92f30 --- /dev/null +++ b/src/mapeditor/UndoRedoManager.ts @@ -0,0 +1,129 @@ +/** + * Defines the return type for undo and redo actions. + */ +export type UndoRedoReturnType = (T | void) | (Promise | Promise); + +export interface EditorAction { + /** + * Describes the undo/redo-able action that has been performed. + */ + description?: string; + /** + * Callback called when an undo or redo action has been performed. + * Typically used to perform an action in both cases (undo and redo). + */ + common?: (actionType: "push" | "undo" | "redo") => UndoRedoReturnType; + /** + * Callback executed when an action should be applied/undone. + */ + apply: () => UndoRedoReturnType; + /** + * Callback called when an action should be redone. + * Calling undoRedo.push(...) will automatically invoke this callback. + */ + redo: () => UndoRedoReturnType; +} + +export class UndoRedoManager { + private currentIndex: number = -1; + private actionStack: EditorAction[] = []; + + /** + * Gets the reference to the current stack of actions. + */ + public get stack(): ReadonlyArray { + return this.actionStack; + } + + /** + * Pushes the given action to the undo/redo stack. If the current action index + * is less than the stack size, the stack will be truncated. + * @param action - The action to push onto the undo/redo stack. + */ + public push(action: EditorAction): UndoRedoReturnType { + // Truncate stack if necessary + if (this.currentIndex < this.actionStack.length - 1) { + this.actionStack.splice(this.currentIndex + 1); + } + + // Push action and invoke redo function + this.actionStack.push(action); + return this.applyAction("push"); + } + + /** + * Undoes the action at the current index of the stack. + * If the action is asynchronous, its promise is returned. + */ + public undo(): UndoRedoReturnType { + return this.performUndo(); + } + + /** + * Redoes the current action at the current index of the stack. + * If the action is asynchronous, its promise is returned. + */ + public redo(): UndoRedoReturnType { + return this.applyAction("redo"); + } + + /** + * Called when an undo action should be performed. + */ + private performUndo(): UndoRedoReturnType { + if (this.currentIndex < 0) { + return (() => {}) as UndoRedoReturnType; + } + + const action = this.actionStack[this.currentIndex]; + const possiblePromise = action.apply(); + + if (possiblePromise instanceof Promise) { + possiblePromise.then(() => { + action.common?.("undo"); + }); + } else { + action.common?.("undo"); + } + + this.currentIndex--; + return possiblePromise as UndoRedoReturnType; + } + + /** + * Called when a redo action should be performed. + */ + private applyAction(actionType: "push" | "redo"): UndoRedoReturnType { + if (this.currentIndex >= this.actionStack.length - 1) { + return (() => {}) as UndoRedoReturnType; + } + + this.currentIndex++; + + const action = this.actionStack[this.currentIndex]; + const possiblePromise = action.redo(); + + if (possiblePromise instanceof Promise) { + possiblePromise.then(() => { + action.common?.(actionType); + }); + } else { + action.common?.(actionType); + } + + return possiblePromise as UndoRedoReturnType; + } + + /** + * Clears the current undo/redo stack. + */ + public clear(): void { + this.actionStack = []; + this.currentIndex = -1; + } +} + +/** + * Shared instance of the undo/redo stack manager. + */ +export const undoRedoManager = new UndoRedoManager(); From 79c67e451ad451eef8c89ec869c16c9ed84ffbd1 Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Wed, 3 Jan 2024 22:40:16 +1300 Subject: [PATCH 3/7] Full tile class implementation --- src/mapeditor/Tile.ts | 285 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/mapeditor/Tile.ts diff --git a/src/mapeditor/Tile.ts b/src/mapeditor/Tile.ts new file mode 100644 index 0000000..8b22b95 --- /dev/null +++ b/src/mapeditor/Tile.ts @@ -0,0 +1,285 @@ +export class Tile { + + private hash: number; + + constructor(hash: number | Tile, y?: number, z?: number) { + if (typeof hash === 'number' && typeof y === 'number' && typeof z === 'number') { + // Constructor with x, y, and z + this.hash = y | (hash << 14) | (z << 28); + } else if (typeof hash === 'number' && typeof y === 'number') { + // Constructor with x and y (default z = 0) + this.hash = y | (hash << 14); + } else if (hash instanceof Tile) { + // Constructor with another Location + this.hash = hash.getWorldY() | (hash.getWorldX() << 14) | (hash.getPlane() << 28); + } else if (typeof hash === 'number') { + // Constructor with a hash + this.hash = (hash >> 14) & 16383 | (hash & 16383) | ((hash >> 28) & 3) << 28; + } else { + throw new Error('Invalid arguments'); + } + } + + static compareDistance(from: Tile): (left: Tile, right: Tile) => number { + return (left, right) => { + return from.getTileDistance(left) - from.getTileDistance(right); + }; + } + + transform(diffX: number, diffY: number, diffZ: number): Tile; + + transform(diffX: number, diffY: number): Tile; + + transform(diffX: number, diffY: number, diffZ?: number): Tile { + if (typeof diffZ === 'number') { + return new Tile(this.getWorldX() + diffX, this.getWorldY() + diffY, this.getPlane() + diffZ); + } else { + return new Tile(this.getWorldX() + diffX, this.getWorldY() + diffY, this.getPlane()); + } + } + + + + translate(x: number, y: number): Tile { + return new Tile(this.getWorldX() + x, this.getWorldY() + y, this.getPlane()); + } + + moveLocation(xOffset: number, yOffset: number, planeOffset: number): Tile { + let x = this.getWorldX(); + let y = this.getWorldY(); + let z = this.getPlane(); + x += xOffset; + y += yOffset; + z += planeOffset; + this.hash = y | (x << 14) | (z << 28); + return this; + } + + setLocation(x: number, y: number, plane: number): void; + + setLocation(hash: number): void; + + setLocation(tile: Tile): void; + + setLocation(x: number | Tile, y?: number, plane?: number): void { + if (typeof x === 'number' && typeof y === 'number' && typeof plane === 'number') { + this.hash = y | (x << 14) | (plane << 28); + } else if (typeof x === 'number') { + this.hash = x; + } else { + this.hash = x.getWorldTileId(); + } + } + + withinDistance(x: number, y: number, distance: number): boolean; + + withinDistance(position: Tile, distance: number): boolean; + + withinDistance(arg1: number | Tile, arg2: number, distance?: number): boolean { + if (typeof arg1 === 'number' && typeof arg2 === 'number' && typeof distance === 'number') { + const deltaX = arg1 - this.getWorldX(); + const deltaY = arg2 - this.getWorldY(); + return deltaX <= distance && deltaX >= -distance && deltaY <= distance && deltaY >= -distance; + } else if (arg1 instanceof Tile && typeof arg2 === 'number') { + const tile = arg1; + if (tile.getPlane() !== this.getPlane()) { + return false; + } + const deltaX = tile.getWorldX() - this.getWorldX(); + const deltaY = tile.getWorldY() - this.getWorldY(); + return deltaX <= arg2 && deltaX >= -arg2 && deltaY <= arg2 && deltaY >= -arg2; + } else { + throw new Error('Invalid arguments'); + } + } + + getWorldX(): number { + return (this.hash >> 14) & 16383; + } + + getWorldY(): number { + return this.hash & 16383; + } + + getPlane(): number { + return (this.hash >> 28) & 3; + } + + getLocalX(l: Tile): number { + return this.getWorldX() - 8 * (l.getChunkX() - 6); + } + + getLocalY(l: Tile): number { + return this.getWorldY() - 8 * (l.getChunkY() - 6); + } + + getMapId(): number { + return ((this.getMapX() << 8) + this.getMapY()); + } + + getLocalTileId(tile: Tile): number { + return ((this.getLocalX(tile) & 7) << 4) | (this.getLocalY(tile) & 7); + } + + hashInRegion(): number { + return ((this.getWorldX() & 63) << 6) | (this.getWorldY() & 63) | ((this.getPlane() & 3) << 12); + } + + get18BitHash(): number { + return this.getWorldY() >> 13 | ((this.getWorldX() >> 13) << 8) | (this.getPlane() << 16); + } + getRegionHash(): number { + return this.getMapY() + (this.getMapX() << 8) + (this.getPlane() << 16); + } + + getChunkHash(): number { + return this.getChunkX() | (this.getChunkY() << 11) | ((this.getPlane() & 3) << 22); + } + + + getChunkX(): number { + return (this.getWorldX() >> 3); + } + + getChunkY(): number { + return (this.getWorldY() >> 3); + } + + getMapX(): number { + return (this.getWorldX() >> 6); + } + + getMapY(): number { + return (this.getWorldY() >> 6); + } + + getXInRegion(): number { + return this.getWorldX() & 63; + } + + getYInRegion(): number { + return this.getWorldY() & 63; + } + + getXInChunk(): number { + return this.getWorldX() & 7; + } + + getYInChunk(): number { + return this.getWorldY() & 7; + } + + getDistance(other: Tile): number; + + getDistance(x: number, y: number): number; + + getDistance(arg1: Tile | number, y?: number): number { + if (arg1 instanceof Tile && typeof y === 'undefined') { + const xdiff = this.getWorldX() - arg1.getWorldX(); + const ydiff = this.getWorldY() - arg1.getWorldY(); + return Math.sqrt(xdiff * xdiff + ydiff * ydiff); + } else if (typeof arg1 === 'number' && typeof y === 'number') { + const xdiff = this.getWorldX() - arg1; + const ydiff = this.getWorldY() - y; + return Math.sqrt(xdiff * xdiff + ydiff * ydiff); + } else { + throw new Error('Invalid arguments'); + } + } + + getTileDistance(other: Tile): number { + const deltaX = other.getWorldX() - this.getWorldX(); + const deltaY = other.getWorldY() - this.getWorldY(); + return Math.max(Math.abs(deltaX), Math.abs(deltaY)); + } + + deltaAbsolute(b: Tile): Tile { + return new Tile(Math.abs(b.getWorldX() - this.getWorldX()), Math.abs(b.getWorldY() - this.getWorldY())); + } + + getWorldTileId(): number { + return this.hash; + } + + equals(x: number, y: number, plane: number): boolean; + + equals(other: Tile): boolean; + + equals(arg1: number | Tile, y?: number, plane?: number): boolean { + if (typeof arg1 === 'number' && typeof y === 'number' && typeof plane === 'number') { + return this.getWorldX() === arg1 && this.getWorldY() === y && this.getPlane() === plane; + } else if (arg1 instanceof Tile && typeof y === 'undefined' && typeof plane === 'undefined') { + return arg1.getWorldTileId() === this.getWorldTileId(); + } else { + throw new Error('Invalid arguments'); + } + } + + toString(): string { + return `Tile: ${this.getWorldX()}, ${this.getWorldY()}, ${this.getPlane()}, region[${this.getMapId()}, ${this.getMapX()}, ${this.getMapY()}], chunk[${this.getChunkX()}, ${this.getChunkY()}], hash [${this.getWorldTileId()}]`; + } + + + distanceWithSize(other: Tile, thisSize: number): number { + if (this.isInside(other, thisSize)) { + return 0; + } + + let minDistance = this.getDistance(other); + + for (let xx = 0; xx < thisSize - 1; xx++) { + const dx = other.getWorldX() - (this.getWorldX() + xx); + const dz = other.getWorldY() - this.getWorldY(); + const distance = Math.sqrt(dx * dx + dz * dz); + if (distance < minDistance) { + minDistance = distance; + } + } + + for (let yy = 0; yy < thisSize - 1; yy++) { + const dx = other.getWorldX() - this.getWorldX(); + const dz = other.getWorldY() - (this.getWorldY() + yy); + const distance = Math.sqrt(dx * dx + dz * dz); + if (distance < minDistance) { + minDistance = distance; + } + } + + for (let xx = 0; xx < thisSize - 1; xx++) { + const dx = other.getWorldX() - (this.getWorldX() + xx); + const dz = other.getWorldY() - (this.getWorldY() + (thisSize - 1)); + const distance = Math.sqrt(dx * dx + dz * dz); + if (distance < minDistance) { + minDistance = distance; + } + } + + for (let yy = 0; yy < thisSize - 1; yy++) { + const dx = other.getWorldX() - (this.getWorldX() + (thisSize - 1)); + const dz = other.getWorldY() - (this.getWorldY() + yy); + const distance = Math.sqrt(dx * dx + dz * dz); + if (distance < minDistance) { + minDistance = distance; + } + } + + return minDistance; + } + + isInside(other: Tile, thisSize: number): boolean { + if (this.equals(other)) { + return true; + } + const insideHorizontal = other.getWorldX() >= this.getWorldX() && other.getWorldX() <= this.getWorldX() + (thisSize - 1); + const insideVertical = other.getWorldY() >= this.getWorldY() && other.getWorldY() <= this.getWorldY() + (thisSize - 1); + return insideVertical && insideHorizontal; + } + + copy(): Tile { + return new Tile(this); + } + + add(other: Tile): Tile { + return new Tile(this.getWorldX() + other.getWorldX(), this.getWorldY() + other.getWorldY()); + } +} From 78e847029a0ec472f3a3c8d7da969a6ee1e62754 Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Wed, 3 Jan 2024 22:40:50 +1300 Subject: [PATCH 4/7] undo redo key bind --- src/mapviewer/InputManager.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/mapviewer/InputManager.ts b/src/mapviewer/InputManager.ts index f45a17e..5bbae72 100644 --- a/src/mapviewer/InputManager.ts +++ b/src/mapviewer/InputManager.ts @@ -1,5 +1,6 @@ import { vec2 } from "gl-matrix"; import { IJoystickUpdateEvent } from "react-joystick-component/build/lib/Joystick"; +import {undoRedoManager} from "../mapeditor/UndoRedoManager"; export function getMousePos(container: HTMLElement, event: MouseEvent | Touch): vec2 { const rect = container.getBoundingClientRect(); @@ -194,6 +195,7 @@ export class InputManager { private onKeyDown = (event: KeyboardEvent) => { event.preventDefault(); this.keys.set(event.code, true); + this.handleUndoRedoShortcuts(); }; private onKeyUp = (event: KeyboardEvent) => { @@ -201,6 +203,24 @@ export class InputManager { this.keys.delete(event.code); }; + + /** + * Handles keyboard shortcuts for undo (Ctrl+Z) and redo (Ctrl+Shift+Z). + */ + private handleUndoRedoShortcuts() { + if (this.isControlDown()) { + if (this.isKeyDownEvent("KeyZ")) { + if (this.isShiftDown()) { + // Ctrl+Shift+Z for redo + undoRedoManager.redo(); + } else { + // Ctrl+Z for undo + undoRedoManager.undo(); + } + } + } + } + private onMouseDown = (event: MouseEvent) => { if (!this.element) { return; From 3e5684e44f4375b97fc2f12cfaf192af5b858245 Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Wed, 3 Jan 2024 23:45:43 +1300 Subject: [PATCH 5/7] test --- src/mapeditor/Tile.ts | 112 +++++++++--------- src/mapeditor/webgl/WebGLMapEditorRenderer.ts | 48 +++++++- src/rs/map/MapFileIndex.ts | 6 + 3 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/mapeditor/Tile.ts b/src/mapeditor/Tile.ts index 8b22b95..b771098 100644 --- a/src/mapeditor/Tile.ts +++ b/src/mapeditor/Tile.ts @@ -1,23 +1,14 @@ export class Tile { - private hash: number; - constructor(hash: number | Tile, y?: number, z?: number) { - if (typeof hash === 'number' && typeof y === 'number' && typeof z === 'number') { - // Constructor with x, y, and z - this.hash = y | (hash << 14) | (z << 28); - } else if (typeof hash === 'number' && typeof y === 'number') { - // Constructor with x and y (default z = 0) - this.hash = y | (hash << 14); - } else if (hash instanceof Tile) { - // Constructor with another Location - this.hash = hash.getWorldY() | (hash.getWorldX() << 14) | (hash.getPlane() << 28); - } else if (typeof hash === 'number') { - // Constructor with a hash - this.hash = (hash >> 14) & 16383 | (hash & 16383) | ((hash >> 28) & 3) << 28; - } else { - throw new Error('Invalid arguments'); + constructor(x: number, y: number, z: number = 0, mapId?: number) { + if (typeof mapId === "number") { + const mapX = (mapId >> 8) & 0xffff; + const mapY = mapId & 255; + x = mapX + x; + y = mapY + y; } + this.hash = y | (x << 14) | (z << 28); } static compareDistance(from: Tile): (left: Tile, right: Tile) => number { @@ -31,15 +22,17 @@ export class Tile { transform(diffX: number, diffY: number): Tile; transform(diffX: number, diffY: number, diffZ?: number): Tile { - if (typeof diffZ === 'number') { - return new Tile(this.getWorldX() + diffX, this.getWorldY() + diffY, this.getPlane() + diffZ); + if (typeof diffZ === "number") { + return new Tile( + this.getWorldX() + diffX, + this.getWorldY() + diffY, + this.getPlane() + diffZ, + ); } else { return new Tile(this.getWorldX() + diffX, this.getWorldY() + diffY, this.getPlane()); } } - - translate(x: number, y: number): Tile { return new Tile(this.getWorldX() + x, this.getWorldY() + y, this.getPlane()); } @@ -62,9 +55,9 @@ export class Tile { setLocation(tile: Tile): void; setLocation(x: number | Tile, y?: number, plane?: number): void { - if (typeof x === 'number' && typeof y === 'number' && typeof plane === 'number') { + if (typeof x === "number" && typeof y === "number" && typeof plane === "number") { this.hash = y | (x << 14) | (plane << 28); - } else if (typeof x === 'number') { + } else if (typeof x === "number") { this.hash = x; } else { this.hash = x.getWorldTileId(); @@ -76,11 +69,16 @@ export class Tile { withinDistance(position: Tile, distance: number): boolean; withinDistance(arg1: number | Tile, arg2: number, distance?: number): boolean { - if (typeof arg1 === 'number' && typeof arg2 === 'number' && typeof distance === 'number') { + if (typeof arg1 === "number" && typeof arg2 === "number" && typeof distance === "number") { const deltaX = arg1 - this.getWorldX(); const deltaY = arg2 - this.getWorldY(); - return deltaX <= distance && deltaX >= -distance && deltaY <= distance && deltaY >= -distance; - } else if (arg1 instanceof Tile && typeof arg2 === 'number') { + return ( + deltaX <= distance && + deltaX >= -distance && + deltaY <= distance && + deltaY >= -distance + ); + } else if (arg1 instanceof Tile && typeof arg2 === "number") { const tile = arg1; if (tile.getPlane() !== this.getPlane()) { return false; @@ -89,7 +87,7 @@ export class Tile { const deltaY = tile.getWorldY() - this.getWorldY(); return deltaX <= arg2 && deltaX >= -arg2 && deltaY <= arg2 && deltaY >= -arg2; } else { - throw new Error('Invalid arguments'); + throw new Error("Invalid arguments"); } } @@ -105,29 +103,28 @@ export class Tile { return (this.hash >> 28) & 3; } - getLocalX(l: Tile): number { - return this.getWorldX() - 8 * (l.getChunkX() - 6); + getLocalX(): number { + return (this.getWorldX() % 64) + 6; } - getLocalY(l: Tile): number { - return this.getWorldY() - 8 * (l.getChunkY() - 6); + getLocalY(): number { + return (this.getWorldY() % 64) + 6; } getMapId(): number { - return ((this.getMapX() << 8) + this.getMapY()); - } - - getLocalTileId(tile: Tile): number { - return ((this.getLocalX(tile) & 7) << 4) | (this.getLocalY(tile) & 7); + return (this.getMapX() << 8) + this.getMapY(); } hashInRegion(): number { - return ((this.getWorldX() & 63) << 6) | (this.getWorldY() & 63) | ((this.getPlane() & 3) << 12); + return ( + ((this.getWorldX() & 63) << 6) | (this.getWorldY() & 63) | ((this.getPlane() & 3) << 12) + ); } get18BitHash(): number { - return this.getWorldY() >> 13 | ((this.getWorldX() >> 13) << 8) | (this.getPlane() << 16); + return (this.getWorldY() >> 13) | ((this.getWorldX() >> 13) << 8) | (this.getPlane() << 16); } + getRegionHash(): number { return this.getMapY() + (this.getMapX() << 8) + (this.getPlane() << 16); } @@ -136,21 +133,20 @@ export class Tile { return this.getChunkX() | (this.getChunkY() << 11) | ((this.getPlane() & 3) << 22); } - getChunkX(): number { - return (this.getWorldX() >> 3); + return this.getWorldX() >> 3; } getChunkY(): number { - return (this.getWorldY() >> 3); + return this.getWorldY() >> 3; } getMapX(): number { - return (this.getWorldX() >> 6); + return this.getWorldX() >> 6; } getMapY(): number { - return (this.getWorldY() >> 6); + return this.getWorldY() >> 6; } getXInRegion(): number { @@ -174,16 +170,16 @@ export class Tile { getDistance(x: number, y: number): number; getDistance(arg1: Tile | number, y?: number): number { - if (arg1 instanceof Tile && typeof y === 'undefined') { + if (arg1 instanceof Tile && typeof y === "undefined") { const xdiff = this.getWorldX() - arg1.getWorldX(); const ydiff = this.getWorldY() - arg1.getWorldY(); return Math.sqrt(xdiff * xdiff + ydiff * ydiff); - } else if (typeof arg1 === 'number' && typeof y === 'number') { + } else if (typeof arg1 === "number" && typeof y === "number") { const xdiff = this.getWorldX() - arg1; const ydiff = this.getWorldY() - y; return Math.sqrt(xdiff * xdiff + ydiff * ydiff); } else { - throw new Error('Invalid arguments'); + throw new Error("Invalid arguments"); } } @@ -194,7 +190,10 @@ export class Tile { } deltaAbsolute(b: Tile): Tile { - return new Tile(Math.abs(b.getWorldX() - this.getWorldX()), Math.abs(b.getWorldY() - this.getWorldY())); + return new Tile( + Math.abs(b.getWorldX() - this.getWorldX()), + Math.abs(b.getWorldY() - this.getWorldY()), + ); } getWorldTileId(): number { @@ -206,12 +205,16 @@ export class Tile { equals(other: Tile): boolean; equals(arg1: number | Tile, y?: number, plane?: number): boolean { - if (typeof arg1 === 'number' && typeof y === 'number' && typeof plane === 'number') { + if (typeof arg1 === "number" && typeof y === "number" && typeof plane === "number") { return this.getWorldX() === arg1 && this.getWorldY() === y && this.getPlane() === plane; - } else if (arg1 instanceof Tile && typeof y === 'undefined' && typeof plane === 'undefined') { + } else if ( + arg1 instanceof Tile && + typeof y === "undefined" && + typeof plane === "undefined" + ) { return arg1.getWorldTileId() === this.getWorldTileId(); } else { - throw new Error('Invalid arguments'); + throw new Error("Invalid arguments"); } } @@ -219,7 +222,6 @@ export class Tile { return `Tile: ${this.getWorldX()}, ${this.getWorldY()}, ${this.getPlane()}, region[${this.getMapId()}, ${this.getMapX()}, ${this.getMapY()}], chunk[${this.getChunkX()}, ${this.getChunkY()}], hash [${this.getWorldTileId()}]`; } - distanceWithSize(other: Tile, thisSize: number): number { if (this.isInside(other, thisSize)) { return 0; @@ -270,13 +272,17 @@ export class Tile { if (this.equals(other)) { return true; } - const insideHorizontal = other.getWorldX() >= this.getWorldX() && other.getWorldX() <= this.getWorldX() + (thisSize - 1); - const insideVertical = other.getWorldY() >= this.getWorldY() && other.getWorldY() <= this.getWorldY() + (thisSize - 1); + const insideHorizontal = + other.getWorldX() >= this.getWorldX() && + other.getWorldX() <= this.getWorldX() + (thisSize - 1); + const insideVertical = + other.getWorldY() >= this.getWorldY() && + other.getWorldY() <= this.getWorldY() + (thisSize - 1); return insideVertical && insideHorizontal; } copy(): Tile { - return new Tile(this); + return new Tile(this.getWorldX(), this.getWorldY(), this.getPlane()); } add(other: Tile): Tile { diff --git a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts index e2df54a..3e7c84b 100644 --- a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts +++ b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts @@ -13,10 +13,12 @@ import PicoGL, { import { newDrawRange } from "../../mapviewer/webgl/DrawRange"; import { createTextureArray } from "../../picogl/PicoTexture"; -import { getMapSquareId } from "../../rs/map/MapFileIndex"; -import { Scene } from "../../rs/scene/Scene"; +import { getMapCoordinates, getMapSquareId } from "../../rs/map/MapFileIndex"; import { clamp } from "../../util/MathUtil"; import { MapEditorRenderer } from "../MapEditorRenderer"; +import { Tile } from "../Tile"; +import { getMapAndTile, getWorldTileIdFromLocal } from "../Tiles"; +import { UndoRedoManager, undoRedoManager } from "../UndoRedoManager"; import { EditorMapSquare } from "./EditorMapSquare"; import { GRID_PROGRAM, @@ -706,6 +708,9 @@ export class WebGLMapEditorRenderer extends MapEditorRenderer { } } + const cachedHeights: { tile: Tile; height: number }[] = []; + const modifiedHeights: { tile: Tile; newHeight: number }[] = []; + if (smoothing) { const worldTileAverageHeightMap = new Map(); for (const [mapId, tileIds] of hoveredTilesMap) { @@ -768,13 +773,48 @@ export class WebGLMapEditorRenderer extends MapEditorRenderer { const height = map.getHeightMapHeight(tileX, tileY); + const tile = new Tile(tileX, tileY, 0, mapId); + cachedHeights.push({ tile, height }); + //todo make all above planes above incremented by the same amount const adjustment = decrement ? -1 : 1; + const newHeight = Math.max(height + adjustment, 0); + modifiedHeights.push({ tile, newHeight }); map.setHeightMapHeight(tileX, tileY, Math.max(height + adjustment, 0)); } - map.updateHeightMapTexture(this.app); - this.updatedTerrainMapIds.add(mapId); } + + undoRedoManager.push({ + common: () => { + for (const [mapId, tileIds] of hoveredTilesMap) { + const map = this.mapManager.getMapById(mapId); + if (!map) continue; + map.updateHeightMapTexture(this.app); + this.updatedTerrainMapIds.add(mapId); + } + }, + apply: () => { + for (const entry of cachedHeights) { + const { tile, height } = entry; + const map = this.mapManager.getMapById(tile.getMapId()); + if (!map) { + continue; + } + map.setHeightMapHeight(tile.getLocalX(), tile.getLocalY(), height); + } + }, + redo: () => { + for (const entry of modifiedHeights) { + const { tile, newHeight } = entry; + const mapId = tile.getMapId(); + const map = this.mapManager.getMapById(mapId); + if (!map) { + continue; + } + map.setHeightMapHeight(tile.getLocalX(), tile.getLocalY(), newHeight); + } + }, + }); } } diff --git a/src/rs/map/MapFileIndex.ts b/src/rs/map/MapFileIndex.ts index 4a34b5b..adfbba4 100644 --- a/src/rs/map/MapFileIndex.ts +++ b/src/rs/map/MapFileIndex.ts @@ -5,6 +5,12 @@ export function getMapSquareId(mapX: number, mapY: number): number { return (mapX << 8) + mapY; } +export function getMapCoordinates(mapId: number): { mapX: number; mapY: number } { + const mapY = mapId & 255; + const mapX = (mapId >> 8) & 0xffff; + return { mapX, mapY }; +} + export interface MapFileIndex { getTerrainArchiveId(mapX: number, mapY: number): number; getLandscapeArchiveId(mapX: number, mapY: number): number; From a423e9ee04501bdbfbec6e0c755ccf9951692aa9 Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Thu, 4 Jan 2024 00:55:27 +1300 Subject: [PATCH 6/7] buggy implementation proof of concept for undo/redo --- src/mapeditor/Tile.ts | 4 ++-- src/mapeditor/UndoRedoManager.ts | 12 ++++++++---- src/mapeditor/webgl/WebGLMapEditorRenderer.ts | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/mapeditor/Tile.ts b/src/mapeditor/Tile.ts index b771098..f8acfd1 100644 --- a/src/mapeditor/Tile.ts +++ b/src/mapeditor/Tile.ts @@ -5,8 +5,8 @@ export class Tile { if (typeof mapId === "number") { const mapX = (mapId >> 8) & 0xffff; const mapY = mapId & 255; - x = mapX + x; - y = mapY + y; + x = mapX * 64 + x - 6; + y = mapY * 64 + y - 6; } this.hash = y | (x << 14) | (z << 28); } diff --git a/src/mapeditor/UndoRedoManager.ts b/src/mapeditor/UndoRedoManager.ts index 9d92f30..85442f7 100644 --- a/src/mapeditor/UndoRedoManager.ts +++ b/src/mapeditor/UndoRedoManager.ts @@ -16,7 +16,7 @@ export interface EditorAction { /** * Callback executed when an action should be applied/undone. */ - apply: () => UndoRedoReturnType; + undo: () => UndoRedoReturnType; /** * Callback called when an action should be redone. * Calling undoRedo.push(...) will automatically invoke this callback. @@ -72,11 +72,13 @@ export class UndoRedoManager { */ private performUndo(): UndoRedoReturnType { if (this.currentIndex < 0) { - return (() => {}) as UndoRedoReturnType; + return (() => { + console.log("can't undo."); + }) as UndoRedoReturnType; } const action = this.actionStack[this.currentIndex]; - const possiblePromise = action.apply(); + const possiblePromise = action.undo(); if (possiblePromise instanceof Promise) { possiblePromise.then(() => { @@ -95,7 +97,9 @@ export class UndoRedoManager { */ private applyAction(actionType: "push" | "redo"): UndoRedoReturnType { if (this.currentIndex >= this.actionStack.length - 1) { - return (() => {}) as UndoRedoReturnType; + return (() => { + console.log("can't do action"); + }) as UndoRedoReturnType; } this.currentIndex++; diff --git a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts index 3e7c84b..af2703a 100644 --- a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts +++ b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts @@ -793,11 +793,12 @@ export class WebGLMapEditorRenderer extends MapEditorRenderer { this.updatedTerrainMapIds.add(mapId); } }, - apply: () => { + undo: () => { for (const entry of cachedHeights) { const { tile, height } = entry; const map = this.mapManager.getMapById(tile.getMapId()); if (!map) { + console.log("undo invalid map " + tile.getMapId()); continue; } map.setHeightMapHeight(tile.getLocalX(), tile.getLocalY(), height); @@ -809,6 +810,7 @@ export class WebGLMapEditorRenderer extends MapEditorRenderer { const mapId = tile.getMapId(); const map = this.mapManager.getMapById(mapId); if (!map) { + console.log("redo invalid map " + tile.getMapId()); continue; } map.setHeightMapHeight(tile.getLocalX(), tile.getLocalY(), newHeight); From 6cf877693f3d0175592641d83609649c67029031 Mon Sep 17 00:00:00 2001 From: tamatea239 Date: Thu, 4 Jan 2024 00:57:05 +1300 Subject: [PATCH 7/7] removed tiles util class --- src/mapeditor/Tiles.ts | 41 ------------------- src/mapeditor/webgl/WebGLMapEditorRenderer.ts | 3 +- 2 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 src/mapeditor/Tiles.ts diff --git a/src/mapeditor/Tiles.ts b/src/mapeditor/Tiles.ts deleted file mode 100644 index fd667cf..0000000 --- a/src/mapeditor/Tiles.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {getMapCoordinates, getMapSquareId} from "../rs/map/MapFileIndex"; - -const borderSize = 6; - -export const getMapAndTile = (mapId: number, tileId: number): number => { - const mapCoordinates = getMapCoordinates(mapId); - - const mapX = mapCoordinates.mapX & 0xFFFF; - const mapY = mapCoordinates.mapY & 0xFFFF; - - const packedTileId = - (BigInt(mapX) << 48n) | (BigInt(mapY) << 32n) | (BigInt(tileId >> 8) << 16n) | BigInt(tileId & 0xFF); - - return Number(packedTileId); -}; - - - -export const getWorldTileId = (worldX: number, worldY: number): number => { - const mapX = Math.floor(worldX / 64); - const mapY = Math.floor(worldY / 64); - const tileX = (worldX % 64) + borderSize; - const tileY = (worldY % 64) + borderSize; - - const packedTileId = - (BigInt(mapX & 0xFFFF) << 48n) | (BigInt(mapY & 0xFFFF) << 32n) | (BigInt(tileX & 0xFFFF) << 16n) | BigInt(tileY & 0xFFFF); - - return Number(packedTileId); -}; - -export const getWorldTileIdFromLocal = (mapId: number, tileX: number, tileY: number): bigint => { - const mapCoordinates = getMapCoordinates(mapId); - - const mapX = BigInt(mapCoordinates.mapX) & 0xFFFFn; - const mapY = BigInt(mapCoordinates.mapY) & 0xFFFFn; - const shiftedMapX = mapX << 32n; - const shiftedMapY = mapY << 16n; - - return shiftedMapX | shiftedMapY | BigInt(tileX & 0xFFFF) << 16n | BigInt(tileY & 0xFFFF); -}; - diff --git a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts index af2703a..60e262a 100644 --- a/src/mapeditor/webgl/WebGLMapEditorRenderer.ts +++ b/src/mapeditor/webgl/WebGLMapEditorRenderer.ts @@ -17,8 +17,7 @@ import { getMapCoordinates, getMapSquareId } from "../../rs/map/MapFileIndex"; import { clamp } from "../../util/MathUtil"; import { MapEditorRenderer } from "../MapEditorRenderer"; import { Tile } from "../Tile"; -import { getMapAndTile, getWorldTileIdFromLocal } from "../Tiles"; -import { UndoRedoManager, undoRedoManager } from "../UndoRedoManager"; +import { undoRedoManager } from "../UndoRedoManager"; import { EditorMapSquare } from "./EditorMapSquare"; import { GRID_PROGRAM,