Add undo/redo support to the editor
This commit is contained in:
parent
884d6d9164
commit
acfad66974
@ -13,19 +13,41 @@ class TileEditorOverlay extends TransientOverlay {
|
|||||||
edit_tile(tile, cell) {
|
edit_tile(tile, cell) {
|
||||||
this.tile = tile;
|
this.tile = tile;
|
||||||
this.cell = cell;
|
this.cell = cell;
|
||||||
|
|
||||||
|
this.needs_undo_entry = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Please call this BEFORE actually modifying the tile; it's important for undo!
|
||||||
mark_dirty() {
|
mark_dirty() {
|
||||||
if (this.cell) {
|
if (this.cell) {
|
||||||
|
if (! this.needs_undo_entry) {
|
||||||
|
// We are ABOUT to mutate this tile for the first time; swap it out with a clone in
|
||||||
|
// preparation for making an undo entry when this overlay closes
|
||||||
|
this.pristine_tile = this.tile;
|
||||||
|
this.tile = {...this.tile};
|
||||||
|
this.cell[this.tile.type.layer] = this.tile;
|
||||||
|
this.needs_undo_entry = true;
|
||||||
|
}
|
||||||
this.editor.mark_cell_dirty(this.cell);
|
this.editor.mark_cell_dirty(this.cell);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TODO i guess i'm just kind of assuming it's the palette selection, but it doesn't
|
// TODO i guess i'm just kind of assuming it's the palette selection, but it doesn't
|
||||||
// necessarily have to be, if i... do something later
|
// necessarily have to be, if i... do something later
|
||||||
this.editor.redraw_palette_selection();
|
// The change hasn't happened yet! Don't redraw until we return to the event loop
|
||||||
|
setTimeout(() => this.editor.redraw_palette_selection(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.needs_undo_entry) {
|
||||||
|
// This will be a no-op the first time since the tile was already swapped, but it's
|
||||||
|
// important for redo
|
||||||
|
this.editor._assign_tile(this.cell, this.tile.type.layer, this.tile, this.pristine_tile);
|
||||||
|
this.editor.commit_undo();
|
||||||
|
}
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
static configure_tile_defaults(tile) {
|
static configure_tile_defaults(tile) {
|
||||||
// FIXME maybe this should be on the tile type, so it functions as documentation there?
|
// FIXME maybe this should be on the tile type, so it functions as documentation there?
|
||||||
}
|
}
|
||||||
@ -57,8 +79,8 @@ class LetterTileEditor extends TileEditorOverlay {
|
|||||||
|
|
||||||
list.addEventListener('change', ev => {
|
list.addEventListener('change', ev => {
|
||||||
if (this.tile) {
|
if (this.tile) {
|
||||||
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
|
|
||||||
this.mark_dirty();
|
this.mark_dirty();
|
||||||
|
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -150,13 +172,13 @@ class FrameBlockTileEditor extends TileEditorOverlay {
|
|||||||
if (! this.tile)
|
if (! this.tile)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.mark_dirty();
|
||||||
if (ev.target.checked) {
|
if (ev.target.checked) {
|
||||||
this.tile.arrows.add(ev.target.value);
|
this.tile.arrows.add(ev.target.value);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.tile.arrows.delete(ev.target.value);
|
this.tile.arrows.delete(ev.target.value);
|
||||||
}
|
}
|
||||||
this.mark_dirty();
|
|
||||||
});
|
});
|
||||||
this.root.append(arrow_list);
|
this.root.append(arrow_list);
|
||||||
}
|
}
|
||||||
@ -201,7 +223,10 @@ class RailroadTileEditor extends TileEditorOverlay {
|
|||||||
track_list.append(mk('li', mk('label', input, svg_icons[i])));
|
track_list.append(mk('li', mk('label', input, svg_icons[i])));
|
||||||
}
|
}
|
||||||
track_list.addEventListener('change', ev => {
|
track_list.addEventListener('change', ev => {
|
||||||
if (this.tile) {
|
if (! this.tile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.mark_dirty();
|
||||||
let bit = 1 << ev.target.value;
|
let bit = 1 << ev.target.value;
|
||||||
if (ev.target.checked) {
|
if (ev.target.checked) {
|
||||||
this.tile.tracks |= bit;
|
this.tile.tracks |= bit;
|
||||||
@ -209,8 +234,6 @@ class RailroadTileEditor extends TileEditorOverlay {
|
|||||||
else {
|
else {
|
||||||
this.tile.tracks &= ~bit;
|
this.tile.tracks &= ~bit;
|
||||||
}
|
}
|
||||||
this.mark_dirty();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.root.append(track_list);
|
this.root.append(track_list);
|
||||||
|
|
||||||
@ -224,8 +247,8 @@ class RailroadTileEditor extends TileEditorOverlay {
|
|||||||
// TODO if they pick a track that's missing it should add it
|
// TODO if they pick a track that's missing it should add it
|
||||||
switch_list.addEventListener('change', ev => {
|
switch_list.addEventListener('change', ev => {
|
||||||
if (this.tile) {
|
if (this.tile) {
|
||||||
this.tile.track_switch = parseInt(ev.target.value, 10);
|
|
||||||
this.mark_dirty();
|
this.mark_dirty();
|
||||||
|
this.tile.track_switch = parseInt(ev.target.value, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.root.append(switch_list);
|
this.root.append(switch_list);
|
||||||
@ -260,6 +283,6 @@ export const TILES_WITH_PROPS = {
|
|||||||
railroad: RailroadTileEditor,
|
railroad: RailroadTileEditor,
|
||||||
// TODO various wireable tiles (hmm not sure how that ui works)
|
// TODO various wireable tiles (hmm not sure how that ui works)
|
||||||
// TODO initial value of counter
|
// TODO initial value of counter
|
||||||
// TODO cloner arrows
|
// TODO cloner arrows (should this be automatic unless you set them explicitly?)
|
||||||
// TODO later, custom floor/wall selection
|
// TODO later, custom floor/wall selection
|
||||||
};
|
};
|
||||||
|
|||||||
@ -270,6 +270,7 @@ class EditorLevelBrowserOverlay extends DialogOverlay {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME ring buffer?
|
||||||
this.undo_stack = [];
|
this.undo_stack = [];
|
||||||
|
|
||||||
// Left buttons
|
// Left buttons
|
||||||
@ -647,11 +648,8 @@ class PencilOperation extends DrawOperation {
|
|||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
// Erase
|
// Erase
|
||||||
if (this.modifier === 'shift') {
|
if (this.modifier === 'shift') {
|
||||||
// Aggressive mode: erase the entire cell
|
let new_cell = this.editor.make_blank_cell(x, y);
|
||||||
for (let layer = 0; layer < LAYERS.MAX; layer++) {
|
this.editor.replace_cell(cell, new_cell);
|
||||||
cell[layer] = null;
|
|
||||||
}
|
|
||||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
|
||||||
}
|
}
|
||||||
else if (template) {
|
else if (template) {
|
||||||
// Erase whatever's on the same layer
|
// Erase whatever's on the same layer
|
||||||
@ -664,24 +662,26 @@ class PencilOperation extends DrawOperation {
|
|||||||
return;
|
return;
|
||||||
if (this.modifier === 'shift') {
|
if (this.modifier === 'shift') {
|
||||||
// Aggressive mode: erase whatever's already in the cell
|
// Aggressive mode: erase whatever's already in the cell
|
||||||
for (let layer = 0; layer < LAYERS.MAX; layer++) {
|
let new_cell = this.editor.make_blank_cell(x, y);
|
||||||
cell[layer] = null;
|
new_cell[template.type.layer] = {...template};
|
||||||
}
|
this.editor.replace_cell(cell, new_cell);
|
||||||
let type = this.editor.palette_selection.type;
|
|
||||||
if (type.layer !== LAYERS.terrain) {
|
|
||||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
|
||||||
}
|
|
||||||
this.editor.place_in_cell(x, y, template);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Default operation: only erase whatever's on the same layer
|
// Default operation: only erase whatever's on the same layer
|
||||||
this.editor.place_in_cell(x, y, template);
|
this.editor.place_in_cell(cell, template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.editor.mark_cell_dirty(cell);
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.editor.commit_undo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO also, delete
|
||||||
|
// TODO also, non-rectangular selections
|
||||||
|
// TODO also, better marching ants, hard to see on gravel
|
||||||
|
// TODO press esc to cancel pending selection
|
||||||
class SelectOperation extends MouseOperation {
|
class SelectOperation extends MouseOperation {
|
||||||
start() {
|
start() {
|
||||||
if (! this.editor.selection.is_empty && this.editor.selection.contains(this.gx0, this.gy0)) {
|
if (! this.editor.selection.is_empty && this.editor.selection.contains(this.gx0, this.gy0)) {
|
||||||
@ -700,7 +700,11 @@ class SelectOperation extends MouseOperation {
|
|||||||
}
|
}
|
||||||
step(mx, my, gxf, gyf, gx, gy) {
|
step(mx, my, gxf, gyf, gx, gy) {
|
||||||
if (this.mode === 'float') {
|
if (this.mode === 'float') {
|
||||||
if (! this.has_moved) {
|
if (this.has_moved) {
|
||||||
|
this.editor.selection.move_by(Math.floor(gx - this.gx1), Math.floor(gy - this.gy1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.make_copy) {
|
if (this.make_copy) {
|
||||||
if (this.editor.selection.is_floating) {
|
if (this.editor.selection.is_floating) {
|
||||||
// Stamp the floating selection but keep it floating
|
// Stamp the floating selection but keep it floating
|
||||||
@ -714,12 +718,9 @@ class SelectOperation extends MouseOperation {
|
|||||||
this.editor.selection.enfloat();
|
this.editor.selection.enfloat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.editor.selection.move_by(Math.floor(gx - this.gx1), Math.floor(gy - this.gy1));
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
this.update_pending_selection();
|
this.update_pending_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.has_moved = true;
|
this.has_moved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,8 +730,20 @@ class SelectOperation extends MouseOperation {
|
|||||||
|
|
||||||
commit() {
|
commit() {
|
||||||
if (this.mode === 'float') {
|
if (this.mode === 'float') {
|
||||||
|
// Make selection move undoable
|
||||||
|
let dx = Math.floor(this.gx1 - this.gx0);
|
||||||
|
let dy = Math.floor(this.gy1 - this.gy0);
|
||||||
|
if (dx || dy) {
|
||||||
|
this.editor._done(
|
||||||
|
() => this.editor.selection.move_by(dx, dy),
|
||||||
|
() => this.editor.selection.move_by(-dx, -dy),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// If there's an existing floating selection (which isn't what we're operating on),
|
||||||
|
// commit it before doing anything else
|
||||||
|
this.editor.selection.defloat();
|
||||||
if (! this.has_moved) {
|
if (! this.has_moved) {
|
||||||
// Plain click clears selection
|
// Plain click clears selection
|
||||||
this.pending_selection.discard();
|
this.pending_selection.discard();
|
||||||
@ -740,10 +753,11 @@ class SelectOperation extends MouseOperation {
|
|||||||
this.pending_selection.commit();
|
this.pending_selection.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.editor.commit_undo();
|
||||||
}
|
}
|
||||||
abort() {
|
abort() {
|
||||||
if (this.mode === 'float') {
|
if (this.mode === 'float') {
|
||||||
// Nothing to do really
|
// FIXME revert the move?
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.pending_selection.discard();
|
this.pending_selection.discard();
|
||||||
@ -754,7 +768,7 @@ class SelectOperation extends MouseOperation {
|
|||||||
class ForceFloorOperation extends DrawOperation {
|
class ForceFloorOperation extends DrawOperation {
|
||||||
start() {
|
start() {
|
||||||
// Begin by placing an all-way force floor under the mouse
|
// Begin by placing an all-way force floor under the mouse
|
||||||
this.editor.place_in_cell(this.gx0, this.gy0, {type: TILE_TYPES.force_floor_all});
|
this.editor.place_in_cell(this.cell(this.gx0, this.gy0), {type: TILE_TYPES.force_floor_all});
|
||||||
}
|
}
|
||||||
step(mx, my, gxf, gyf) {
|
step(mx, my, gxf, gyf) {
|
||||||
// Walk the mouse movement and change each we touch to match the direction we
|
// Walk the mouse movement and change each we touch to match the direction we
|
||||||
@ -795,7 +809,7 @@ class ForceFloorOperation extends DrawOperation {
|
|||||||
if (i === 2) {
|
if (i === 2) {
|
||||||
let prevcell = this.editor.cell(prevx, prevy);
|
let prevcell = this.editor.cell(prevx, prevy);
|
||||||
if (prevcell[LAYERS.terrain].type.name.startsWith('force_floor_')) {
|
if (prevcell[LAYERS.terrain].type.name.startsWith('force_floor_')) {
|
||||||
this.editor.place_in_cell(prevcell.x, prevcell.y, {type: TILE_TYPES[name]});
|
this.editor.place_in_cell(prevcell, {type: TILE_TYPES[name]});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -807,12 +821,15 @@ class ForceFloorOperation extends DrawOperation {
|
|||||||
{
|
{
|
||||||
name = 'ice';
|
name = 'ice';
|
||||||
}
|
}
|
||||||
this.editor.place_in_cell(x, y, {type: TILE_TYPES[name]});
|
this.editor.place_in_cell(cell, {type: TILE_TYPES[name]});
|
||||||
|
|
||||||
prevx = x;
|
prevx = x;
|
||||||
prevy = y;
|
prevy = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cleanup() {
|
||||||
|
this.editor.commit_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO entered cell should get blank railroad?
|
// TODO entered cell should get blank railroad?
|
||||||
@ -878,23 +895,24 @@ class TrackOperation extends DrawOperation {
|
|||||||
let cell = this.cell(prevx, prevy);
|
let cell = this.cell(prevx, prevy);
|
||||||
let terrain = cell[0];
|
let terrain = cell[0];
|
||||||
if (terrain.type.name === 'railroad') {
|
if (terrain.type.name === 'railroad') {
|
||||||
|
let new_terrain = {...terrain};
|
||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
// Erase
|
// Erase
|
||||||
// TODO fix track switch?
|
// TODO fix track switch?
|
||||||
// TODO if this leaves tracks === 0, replace with floor?
|
// TODO if this leaves tracks === 0, replace with floor?
|
||||||
terrain.tracks &= ~bit;
|
new_terrain.tracks &= ~bit;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Draw
|
// Draw
|
||||||
terrain.tracks |= bit;
|
new_terrain.tracks |= bit;
|
||||||
}
|
}
|
||||||
this.editor.mark_cell_dirty(cell);
|
this.editor.place_in_cell(cell, new_terrain);
|
||||||
}
|
}
|
||||||
else if (! this.alt_mode) {
|
else if (! this.alt_mode) {
|
||||||
terrain = { type: TILE_TYPES['railroad'] };
|
terrain = { type: TILE_TYPES['railroad'] };
|
||||||
terrain.type.populate_defaults(terrain);
|
terrain.type.populate_defaults(terrain);
|
||||||
terrain.tracks |= bit;
|
terrain.tracks |= bit;
|
||||||
this.editor.place_in_cell(prevx, prevy, terrain);
|
this.editor.place_in_cell(cell, terrain);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevx = x;
|
prevx = x;
|
||||||
@ -902,6 +920,9 @@ class TrackOperation extends DrawOperation {
|
|||||||
this.entry_direction = DIRECTIONS[exit_direction].opposite;
|
this.entry_direction = DIRECTIONS[exit_direction].opposite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cleanup() {
|
||||||
|
this.editor.commit_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WireOperation extends DrawOperation {
|
class WireOperation extends DrawOperation {
|
||||||
@ -936,15 +957,16 @@ class WireOperation extends DrawOperation {
|
|||||||
|
|
||||||
let terrain = cell[LAYERS.terrain];
|
let terrain = cell[LAYERS.terrain];
|
||||||
if (terrain.type.name === 'floor') {
|
if (terrain.type.name === 'floor') {
|
||||||
|
terrain = {...terrain};
|
||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
terrain.wire_tunnel_directions &= ~bit;
|
terrain.wire_tunnel_directions &= ~bit;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
terrain.wire_tunnel_directions |= bit;
|
terrain.wire_tunnel_directions |= bit;
|
||||||
}
|
}
|
||||||
this.editor.mark_cell_dirty(cell);
|
this.editor.place_in_cell(cell, terrain);
|
||||||
|
this.editor.commit_undo();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step(mx, my, gxf, gyf) {
|
step(mx, my, gxf, gyf) {
|
||||||
@ -1053,6 +1075,7 @@ class WireOperation extends DrawOperation {
|
|||||||
if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block'].indexOf(tile.type.name) < 0)
|
if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block'].indexOf(tile.type.name) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
tile = {...tile};
|
||||||
tile.wire_directions = tile.wire_directions ?? 0;
|
tile.wire_directions = tile.wire_directions ?? 0;
|
||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
// Erase
|
// Erase
|
||||||
@ -1062,7 +1085,7 @@ class WireOperation extends DrawOperation {
|
|||||||
// Draw
|
// Draw
|
||||||
tile.wire_directions |= DIRECTIONS[wire_direction].bit;
|
tile.wire_directions |= DIRECTIONS[wire_direction].bit;
|
||||||
}
|
}
|
||||||
this.editor.mark_cell_dirty(cell);
|
this.editor.place_in_cell(cell, tile);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1070,6 +1093,9 @@ class WireOperation extends DrawOperation {
|
|||||||
prevqy = qy;
|
prevqy = qy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cleanup() {
|
||||||
|
this.editor.commit_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tiles the "adjust" tool will turn into each other
|
// Tiles the "adjust" tool will turn into each other
|
||||||
@ -1098,6 +1124,7 @@ const ADJUST_TOGGLES_CCW = {};
|
|||||||
['no_player1_sign', 'no_player2_sign'],
|
['no_player1_sign', 'no_player2_sign'],
|
||||||
['flame_jet_off', 'flame_jet_on'],
|
['flame_jet_off', 'flame_jet_on'],
|
||||||
['light_switch_off', 'light_switch_on'],
|
['light_switch_off', 'light_switch_on'],
|
||||||
|
['stopwatch_bonus', 'stopwatch_penalty'],
|
||||||
])
|
])
|
||||||
{
|
{
|
||||||
for (let [i, tile] of cycle.entries()) {
|
for (let [i, tile] of cycle.entries()) {
|
||||||
@ -1127,6 +1154,7 @@ class AdjustOperation extends MouseOperation {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
let rotated;
|
let rotated;
|
||||||
|
tile = {...tile}; // TODO little inefficient
|
||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
// Reverse, go counterclockwise
|
// Reverse, go counterclockwise
|
||||||
rotated = this.editor.rotate_tile_left(tile);
|
rotated = this.editor.rotate_tile_left(tile);
|
||||||
@ -1135,7 +1163,8 @@ class AdjustOperation extends MouseOperation {
|
|||||||
rotated = this.editor.rotate_tile_right(tile);
|
rotated = this.editor.rotate_tile_right(tile);
|
||||||
}
|
}
|
||||||
if (rotated) {
|
if (rotated) {
|
||||||
this.editor.mark_cell_dirty(cell);
|
this.editor.place_in_cell(cell, tile);
|
||||||
|
this.editor.commit_undo();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1143,7 +1172,8 @@ class AdjustOperation extends MouseOperation {
|
|||||||
let other = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name];
|
let other = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name];
|
||||||
if (other) {
|
if (other) {
|
||||||
tile.type = TILE_TYPES[other];
|
tile.type = TILE_TYPES[other];
|
||||||
this.editor.mark_cell_dirty(cell);
|
this.editor.place_in_cell(cell, tile);
|
||||||
|
this.editor.commit_undo();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1154,6 +1184,8 @@ class AdjustOperation extends MouseOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
|
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
|
||||||
|
// FIXME undo
|
||||||
|
// TODO view is not especially visible
|
||||||
class CameraOperation extends MouseOperation {
|
class CameraOperation extends MouseOperation {
|
||||||
start(ev) {
|
start(ev) {
|
||||||
this.offset_x = 0;
|
this.offset_x = 0;
|
||||||
@ -2464,12 +2496,26 @@ class Selection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_pending() {
|
create_pending() {
|
||||||
this.defloat();
|
|
||||||
|
|
||||||
return new PendingSelection(this);
|
return new PendingSelection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_from_rect(rect) {
|
set_from_rect(rect) {
|
||||||
|
let old_rect = this.rect;
|
||||||
|
this.editor._do(
|
||||||
|
() => this._set_from_rect(rect),
|
||||||
|
() => {
|
||||||
|
if (old_rect) {
|
||||||
|
this._set_from_rect(old_rect);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_set_from_rect(rect) {
|
||||||
this.rect = rect;
|
this.rect = rect;
|
||||||
this.element.classList.add('--visible');
|
this.element.classList.add('--visible');
|
||||||
this.element.setAttribute('x', this.rect.x);
|
this.element.setAttribute('x', this.rect.x);
|
||||||
@ -2495,28 +2541,42 @@ class Selection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.defloat();
|
let rect = this.rect;
|
||||||
|
if (! rect)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.editor._do(
|
||||||
|
() => this._clear(),
|
||||||
|
() => this._set_from_rect(rect),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clear() {
|
||||||
this.rect = null;
|
this.rect = null;
|
||||||
this.element.classList.remove('--visible');
|
this.element.classList.remove('--visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
*iter_cells() {
|
*iter_coords() {
|
||||||
if (! this.rect)
|
if (! this.rect)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
let stored_level = this.editor.stored_level;
|
||||||
for (let x = this.rect.left; x < this.rect.right; x++) {
|
for (let x = this.rect.left; x < this.rect.right; x++) {
|
||||||
for (let y = this.rect.top; y < this.rect.bottom; y++) {
|
for (let y = this.rect.top; y < this.rect.bottom; y++) {
|
||||||
yield [x, y];
|
let n = stored_level.coords_to_scalar(x, y);
|
||||||
|
yield [x, y, n];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert this selection into a floating selection, plucking all the selected cells from the
|
||||||
|
// level and replacing them with blank cells.
|
||||||
enfloat(copy = false) {
|
enfloat(copy = false) {
|
||||||
if (this.floated_cells)
|
if (this.floated_cells)
|
||||||
console.error("Trying to float a selection that's already floating");
|
console.error("Trying to float a selection that's already floating");
|
||||||
|
|
||||||
this.floated_cells = [];
|
let floated_cells = [];
|
||||||
let tileset = this.editor.renderer.tileset;
|
let tileset = this.editor.renderer.tileset;
|
||||||
let stored_level = this.editor.stored_level;
|
let stored_level = this.editor.stored_level;
|
||||||
let bbox = this.rect;
|
let bbox = this.rect;
|
||||||
@ -2526,24 +2586,33 @@ class Selection {
|
|||||||
this.editor.renderer.canvas,
|
this.editor.renderer.canvas,
|
||||||
bbox.x * tileset.size_x, bbox.y * tileset.size_y, bbox.width * tileset.size_x, bbox.height * tileset.size_y,
|
bbox.x * tileset.size_x, bbox.y * tileset.size_y, bbox.width * tileset.size_x, bbox.height * tileset.size_y,
|
||||||
0, 0, bbox.width * tileset.size_x, bbox.height * tileset.size_y);
|
0, 0, bbox.width * tileset.size_x, bbox.height * tileset.size_y);
|
||||||
for (let [x, y] of this.iter_cells()) {
|
for (let [x, y, n] of this.iter_coords()) {
|
||||||
let n = stored_level.coords_to_scalar(x, y);
|
let cell = stored_level.linear_cells[n];
|
||||||
if (copy) {
|
if (copy) {
|
||||||
this.floated_cells.push(stored_level.linear_cells[n].map(tile => tile ? {...tile} : null));
|
floated_cells.push(cell.map(tile => tile ? {...tile} : null));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.floated_cells.push(stored_level.linear_cells[n]);
|
floated_cells.push(cell);
|
||||||
stored_level.linear_cells[n] = this.editor._make_cell(x, y);
|
this.editor.replace_cell(cell, this.editor.make_blank_cell(x, y));
|
||||||
this.editor.mark_cell_dirty(stored_level.linear_cells[n]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.floated_element = mk_svg('g', mk_svg('foreignObject', {
|
let floated_element = mk_svg('g', mk_svg('foreignObject', {
|
||||||
x: 0, y: 0,
|
x: 0, y: 0,
|
||||||
width: canvas.width, height: canvas.height,
|
width: canvas.width, height: canvas.height,
|
||||||
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
||||||
}, canvas));
|
}, canvas));
|
||||||
this.floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
||||||
this.svg_group.append(this.floated_element);
|
|
||||||
|
// FIXME far more memory efficient to recreate the canvas in the redo, rather than hold onto
|
||||||
|
// it forever
|
||||||
|
this.editor._do(
|
||||||
|
() => {
|
||||||
|
this.floated_element = floated_element;
|
||||||
|
this.floated_cells = floated_cells;
|
||||||
|
this.svg_group.append(floated_element);
|
||||||
|
},
|
||||||
|
() => this._defloat(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
stamp_float(copy = false) {
|
stamp_float(copy = false) {
|
||||||
@ -2553,16 +2622,14 @@ class Selection {
|
|||||||
let bbox = this.rect;
|
let bbox = this.rect;
|
||||||
let stored_level = this.editor.stored_level;
|
let stored_level = this.editor.stored_level;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let [x, y] of this.iter_cells()) {
|
for (let [x, y, n] of this.iter_coords()) {
|
||||||
let n = stored_level.coords_to_scalar(x, y);
|
|
||||||
let cell = this.floated_cells[i];
|
let cell = this.floated_cells[i];
|
||||||
if (copy) {
|
if (copy) {
|
||||||
cell = cell.map(tile => tile ? {...tile} : null);
|
cell = cell.map(tile => tile ? {...tile} : null);
|
||||||
}
|
}
|
||||||
cell.x = x;
|
cell.x = x;
|
||||||
cell.y = y;
|
cell.y = y;
|
||||||
stored_level.linear_cells[n] = cell;
|
this.editor.replace_cell(stored_level.linear_cells[n], cell);
|
||||||
this.editor.mark_cell_dirty(cell);
|
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2572,6 +2639,21 @@ class Selection {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
this.stamp_float();
|
this.stamp_float();
|
||||||
|
|
||||||
|
let element = this.floated_element;
|
||||||
|
let cells = this.floated_cells;
|
||||||
|
this.editor._do(
|
||||||
|
() => this._defloat(),
|
||||||
|
() => {
|
||||||
|
this.floated_cells = cells;
|
||||||
|
this.floated_element = element;
|
||||||
|
this.svg_group.append(element);
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_defloat() {
|
||||||
this.floated_element.remove();
|
this.floated_element.remove();
|
||||||
this.floated_element = null;
|
this.floated_element = null;
|
||||||
this.floated_cells = null;
|
this.floated_cells = null;
|
||||||
@ -2801,6 +2883,12 @@ export class Editor extends PrimaryView {
|
|||||||
button_container.append(button);
|
button_container.append(button);
|
||||||
return button;
|
return button;
|
||||||
};
|
};
|
||||||
|
this.undo_button = _make_button("Undo", ev => {
|
||||||
|
this.undo();
|
||||||
|
});
|
||||||
|
this.redo_button = _make_button("Redo", ev => {
|
||||||
|
this.redo();
|
||||||
|
});
|
||||||
_make_button("Pack properties...", ev => {
|
_make_button("Pack properties...", ev => {
|
||||||
new EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open();
|
new EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open();
|
||||||
});
|
});
|
||||||
@ -2965,6 +3053,8 @@ export class Editor extends PrimaryView {
|
|||||||
this.select_palette('floor', true);
|
this.select_palette('floor', true);
|
||||||
|
|
||||||
this.selection = new Selection(this);
|
this.selection = new Selection(this);
|
||||||
|
|
||||||
|
this.reset_undo();
|
||||||
}
|
}
|
||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
@ -2981,9 +3071,10 @@ export class Editor extends PrimaryView {
|
|||||||
super.deactivate();
|
super.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
// Level creation, management, and saving
|
// Level creation, management, and saving
|
||||||
|
|
||||||
_make_cell(x, y) {
|
make_blank_cell(x, y) {
|
||||||
let cell = new format_base.StoredCell;
|
let cell = new format_base.StoredCell;
|
||||||
cell.x = x;
|
cell.x = x;
|
||||||
cell.y = y;
|
cell.y = y;
|
||||||
@ -2998,7 +3089,7 @@ export class Editor extends PrimaryView {
|
|||||||
stored_level.size_y = size_y;
|
stored_level.size_y = size_y;
|
||||||
stored_level.viewport_size = 10;
|
stored_level.viewport_size = 10;
|
||||||
for (let i = 0; i < size_x * size_y; i++) {
|
for (let i = 0; i < size_x * size_y; i++) {
|
||||||
stored_level.linear_cells.push(this._make_cell(...stored_level.scalar_to_coords(i)));
|
stored_level.linear_cells.push(this.make_blank_cell(...stored_level.scalar_to_coords(i)));
|
||||||
}
|
}
|
||||||
stored_level.linear_cells[0][LAYERS.actor] = {type: TILE_TYPES['player'], direction: 'south'};
|
stored_level.linear_cells[0][LAYERS.actor] = {type: TILE_TYPES['player'], direction: 'south'};
|
||||||
return stored_level;
|
return stored_level;
|
||||||
@ -3240,6 +3331,8 @@ export class Editor extends PrimaryView {
|
|||||||
{
|
{
|
||||||
this.conductor.level_index += delta;
|
this.conductor.level_index += delta;
|
||||||
// Update the current level if it's not stored in the metadata yet
|
// Update the current level if it's not stored in the metadata yet
|
||||||
|
// FIXME if you delete the level before the current one, this gets decremented twice?
|
||||||
|
// can't seem to reproduce
|
||||||
if (! stored_level) {
|
if (! stored_level) {
|
||||||
this.conductor.stored_level.index += delta;
|
this.conductor.stored_level.index += delta;
|
||||||
this.conductor.stored_level.number += delta;
|
this.conductor.stored_level.number += delta;
|
||||||
@ -3293,6 +3386,7 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
// Level loading
|
// Level loading
|
||||||
|
|
||||||
load_game(stored_game) {
|
load_game(stored_game) {
|
||||||
@ -3314,6 +3408,7 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load connections
|
// Load connections
|
||||||
|
// TODO cloners too
|
||||||
this.connections_g.textContent = '';
|
this.connections_g.textContent = '';
|
||||||
for (let [src, dest] of Object.entries(this.stored_level.custom_trap_wiring)) {
|
for (let [src, dest] of Object.entries(this.stored_level.custom_trap_wiring)) {
|
||||||
let [sx, sy] = this.stored_level.scalar_to_coords(src);
|
let [sx, sy] = this.stored_level.scalar_to_coords(src);
|
||||||
@ -3337,6 +3432,11 @@ export class Editor extends PrimaryView {
|
|||||||
if (this.save_button) {
|
if (this.save_button) {
|
||||||
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
|
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._done_setup) {
|
||||||
|
// XXX this doesn't work yet if setup hasn't run because the undo button won't exist
|
||||||
|
this.reset_undo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_cell_coordinates() {
|
update_cell_coordinates() {
|
||||||
@ -3351,6 +3451,8 @@ export class Editor extends PrimaryView {
|
|||||||
this.svg_overlay.setAttribute('viewBox', `0 0 ${this.stored_level.size_x} ${this.stored_level.size_y}`);
|
this.svg_overlay.setAttribute('viewBox', `0 0 ${this.stored_level.size_x} ${this.stored_level.size_y}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
open_level_browser() {
|
open_level_browser() {
|
||||||
if (! this._level_browser) {
|
if (! this._level_browser) {
|
||||||
this._level_browser = new EditorLevelBrowserOverlay(this.conductor);
|
this._level_browser = new EditorLevelBrowserOverlay(this.conductor);
|
||||||
@ -3478,7 +3580,8 @@ export class Editor extends PrimaryView {
|
|||||||
this.palette_actor_direction = DIRECTIONS[this.palette_actor_direction].left;
|
this.palette_actor_direction = DIRECTIONS[this.palette_actor_direction].left;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Drawing --
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Drawing
|
||||||
|
|
||||||
redraw_palette_selection() {
|
redraw_palette_selection() {
|
||||||
// FIXME should redraw in an existing canvas
|
// FIXME should redraw in an existing canvas
|
||||||
@ -3488,24 +3591,28 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mark_cell_dirty(cell) {
|
mark_cell_dirty(cell) {
|
||||||
|
this.mark_point_dirty(cell.x, cell.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_point_dirty(x, y) {
|
||||||
if (! this._dirty_rect) {
|
if (! this._dirty_rect) {
|
||||||
this._dirty_rect = new DOMRect(cell.x, cell.y, 1, 1);
|
this._dirty_rect = new DOMRect(x, y, 1, 1);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let rect = this._dirty_rect;
|
let rect = this._dirty_rect;
|
||||||
if (cell.x < rect.left) {
|
if (x < rect.left) {
|
||||||
rect.width = rect.right - cell.x;
|
rect.width = rect.right - x;
|
||||||
rect.x = cell.x;
|
rect.x = x;
|
||||||
}
|
}
|
||||||
else if (cell.x >= rect.right) {
|
else if (x >= rect.right) {
|
||||||
rect.width = cell.x - rect.left + 1;
|
rect.width = x - rect.left + 1;
|
||||||
}
|
}
|
||||||
if (cell.y < rect.top) {
|
if (y < rect.top) {
|
||||||
rect.height = rect.bottom - cell.y;
|
rect.height = rect.bottom - y;
|
||||||
rect.y = cell.y;
|
rect.y = y;
|
||||||
}
|
}
|
||||||
else if (cell.y >= rect.bottom) {
|
else if (y >= rect.bottom) {
|
||||||
rect.height = cell.y - rect.top + 1;
|
rect.height = y - rect.top + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3530,7 +3637,8 @@ export class Editor extends PrimaryView {
|
|||||||
this._schedule_redraw_loop();
|
this._schedule_redraw_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Utility/inspection --
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Utility/inspection
|
||||||
|
|
||||||
is_in_bounds(x, y) {
|
is_in_bounds(x, y) {
|
||||||
return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y;
|
return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y;
|
||||||
@ -3545,39 +3653,39 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Mutation --
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Mutation
|
||||||
|
|
||||||
place_in_cell(x, y, tile) {
|
// DOES NOT commit the undo entry!
|
||||||
|
place_in_cell(cell, tile) {
|
||||||
// TODO weird api?
|
// TODO weird api?
|
||||||
if (! tile)
|
if (! tile)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (! this.selection.contains(x, y))
|
if (! this.selection.contains(cell.x, cell.y))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let cell = this.cell(x, y);
|
|
||||||
this.mark_cell_dirty(cell);
|
|
||||||
// Replace whatever's on the same layer
|
// Replace whatever's on the same layer
|
||||||
// TODO should preserve wiring if possible too
|
// TODO should preserve wiring if possible too
|
||||||
let existing_tile = cell[tile.type.layer];
|
let layer = tile.type.layer;
|
||||||
if (existing_tile) {
|
let existing_tile = cell[layer];
|
||||||
// If we find a tile of the same type as the one being drawn, see if it has custom
|
|
||||||
// combine behavior (only the case if it came from the palette)
|
// If we find a tile of the same type as the one being drawn, see if it has custom combine
|
||||||
if (existing_tile.type === tile.type &&
|
// behavior (only the case if it came from the palette)
|
||||||
|
if (existing_tile && existing_tile.type === tile.type &&
|
||||||
// FIXME this is hacky garbage
|
// FIXME this is hacky garbage
|
||||||
tile === this.palette_selection && this.palette_selection_from_palette &&
|
tile === this.palette_selection && this.palette_selection_from_palette &&
|
||||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
||||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw)
|
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw)
|
||||||
{
|
{
|
||||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, existing_tile);
|
let old_tile = {...existing_tile};
|
||||||
|
let new_tile = existing_tile;
|
||||||
|
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, new_tile);
|
||||||
|
this._assign_tile(cell, layer, new_tile, old_tile);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise erase it
|
this._assign_tile(cell, layer, {...tile}, existing_tile);
|
||||||
cell[tile.type.layer] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
cell[tile.type.layer] = {...tile};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
erase_tile(cell, tile = null) {
|
erase_tile(cell, tile = null) {
|
||||||
@ -3589,10 +3697,10 @@ export class Editor extends PrimaryView {
|
|||||||
|
|
||||||
this.mark_cell_dirty(cell);
|
this.mark_cell_dirty(cell);
|
||||||
let existing_tile = cell[tile.type.layer];
|
let existing_tile = cell[tile.type.layer];
|
||||||
if (existing_tile) {
|
|
||||||
// If we find a tile of the same type as the one being drawn, see if it has custom
|
// If we find a tile of the same type as the one being drawn, see if it has custom combine
|
||||||
// combine behavior (only the case if it came from the palette)
|
// behavior (only the case if it came from the palette)
|
||||||
if (existing_tile.type === tile.type &&
|
if (existing_tile && existing_tile.type === tile.type &&
|
||||||
// FIXME this is hacky garbage
|
// FIXME this is hacky garbage
|
||||||
tile === this.palette_selection && this.palette_selection_from_palette &&
|
tile === this.palette_selection && this.palette_selection_from_palette &&
|
||||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
||||||
@ -3603,17 +3711,154 @@ export class Editor extends PrimaryView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise erase it
|
let new_tile = null;
|
||||||
cell[tile.type.layer] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow erasing the floor entirely
|
|
||||||
if (tile.type.layer === LAYERS.terrain) {
|
if (tile.type.layer === LAYERS.terrain) {
|
||||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
new_tile = {type: TILE_TYPES.floor};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._assign_tile(cell, tile.type.layer, new_tile, existing_tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_cell(cell, new_cell) {
|
||||||
|
// Save the coordinates so it doesn't matter what they are when undoing
|
||||||
|
let x = cell.x, y = cell.y;
|
||||||
|
let n = this.stored_level.coords_to_scalar(x, y);
|
||||||
|
this._do(
|
||||||
|
() => {
|
||||||
|
this.stored_level.linear_cells[n] = new_cell;
|
||||||
|
new_cell.x = x;
|
||||||
|
new_cell.y = y;
|
||||||
|
this.mark_cell_dirty(new_cell);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.stored_level.linear_cells[n] = cell;
|
||||||
|
cell.x = x;
|
||||||
|
cell.y = y;
|
||||||
|
this.mark_cell_dirty(cell);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize_level(size_x, size_y, x0 = 0, y0 = 0) {
|
||||||
|
let new_cells = [];
|
||||||
|
for (let y = y0; y < y0 + size_y; y++) {
|
||||||
|
for (let x = x0; x < x0 + size_x; x++) {
|
||||||
|
new_cells.push(this.cell(x, y) ?? this.make_blank_cell(x, y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Misc?? --
|
let original_cells = this.stored_level.linear_cells;
|
||||||
|
let original_size_x = this.stored_level.size_x;
|
||||||
|
let original_size_y = this.stored_level.size_y;
|
||||||
|
|
||||||
|
this._do(() => {
|
||||||
|
this.stored_level.linear_cells = new_cells;
|
||||||
|
this.stored_level.size_x = size_x;
|
||||||
|
this.stored_level.size_y = size_y;
|
||||||
|
this.update_viewport_size();
|
||||||
|
this.update_cell_coordinates();
|
||||||
|
this.redraw_entire_level();
|
||||||
|
}, () => {
|
||||||
|
this.stored_level.linear_cells = original_cells;
|
||||||
|
this.stored_level.size_x = original_size_x;
|
||||||
|
this.stored_level.size_y = original_size_y;
|
||||||
|
this.update_viewport_size();
|
||||||
|
this.update_cell_coordinates();
|
||||||
|
this.redraw_entire_level();
|
||||||
|
});
|
||||||
|
this.commit_undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Undo/redo
|
||||||
|
|
||||||
|
_do(redo, undo, modifies = true) {
|
||||||
|
redo();
|
||||||
|
this._done(redo, undo, modifies);
|
||||||
|
}
|
||||||
|
|
||||||
|
_done(redo, undo, modifies = true) {
|
||||||
|
// TODO parallel arrays would be smaller
|
||||||
|
this.undo_entry.push([undo, redo]);
|
||||||
|
|
||||||
|
if (this.redo_stack.length > 0) {
|
||||||
|
this.redo_stack.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_assign_tile(cell, layer, new_tile, old_tile) {
|
||||||
|
this._do(
|
||||||
|
() => {
|
||||||
|
cell[layer] = new_tile;
|
||||||
|
this.mark_cell_dirty(cell);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
cell[layer] = old_tile;
|
||||||
|
this.mark_cell_dirty(cell);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_undo() {
|
||||||
|
this.undo_entry = [];
|
||||||
|
this.undo_stack = [];
|
||||||
|
this.redo_stack = [];
|
||||||
|
this._update_undo_redo_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
// We shouldn't really have an uncommitted entry lying around at a time when the user can
|
||||||
|
// click the undo button, but just in case, prefer that to the undo stack
|
||||||
|
let entry;
|
||||||
|
if (this.undo_entry.length > 0) {
|
||||||
|
entry = this.undo_entry;
|
||||||
|
this.undo_entry = [];
|
||||||
|
}
|
||||||
|
else if (this.undo_stack.length > 0) {
|
||||||
|
entry = this.undo_stack.pop();
|
||||||
|
this.redo_stack.push(entry);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = entry.length - 1; i >= 0; i--) {
|
||||||
|
entry[i][0]();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._update_undo_redo_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if (this.redo_stack.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.commit_undo();
|
||||||
|
let entry = this.redo_stack.pop();
|
||||||
|
this.undo_stack.push(entry);
|
||||||
|
for (let [undo, redo] of entry) {
|
||||||
|
redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._update_undo_redo_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
commit_undo() {
|
||||||
|
if (this.undo_entry.length > 0) {
|
||||||
|
this.undo_stack.push(this.undo_entry);
|
||||||
|
this.undo_entry = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._update_undo_redo_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update_undo_redo_enabled() {
|
||||||
|
this.undo_button.disabled = this.undo_stack.length === 0;
|
||||||
|
this.redo_button.disabled = this.redo_stack.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// Misc UI stuff
|
||||||
|
|
||||||
open_tile_prop_overlay(tile, cell, rect) {
|
open_tile_prop_overlay(tile, cell, rect) {
|
||||||
this.cancel_mouse_operation();
|
this.cancel_mouse_operation();
|
||||||
@ -3658,20 +3903,4 @@ export class Editor extends PrimaryView {
|
|||||||
this.mouse_op = null;
|
this.mouse_op = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize_level(size_x, size_y, x0 = 0, y0 = 0) {
|
|
||||||
let new_cells = [];
|
|
||||||
for (let y = y0; y < y0 + size_y; y++) {
|
|
||||||
for (let x = x0; x < x0 + size_x; x++) {
|
|
||||||
new_cells.push(this.cell(x, y) ?? this._make_cell(x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stored_level.linear_cells = new_cells;
|
|
||||||
this.stored_level.size_x = size_x;
|
|
||||||
this.stored_level.size_y = size_y;
|
|
||||||
this.update_viewport_size();
|
|
||||||
this.update_cell_coordinates();
|
|
||||||
this.redraw_entire_level();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user