Ditch the textured backgrounds (hard to read), add some icons for the controls, and recolor the icons themselves to the new tentative palette.
1316 lines
46 KiB
JavaScript
1316 lines
46 KiB
JavaScript
// Types that handle mouse activity for a given tool, whether the mouse button is current held or
|
||
// not. (When the mouse button is /not/ held, then only the operation bound to the left mouse
|
||
// button gets events.)
|
||
import { DIRECTIONS, LAYERS } from '../defs.js';
|
||
import TILE_TYPES from '../tiletypes.js';
|
||
import { mk, mk_svg, walk_grid } from '../util.js';
|
||
|
||
import { SVGConnection } from './helpers.js';
|
||
import { TILES_WITH_PROPS } from './tile-overlays.js';
|
||
|
||
// TODO some minor grievances
|
||
// - the track overlay doesn't explain "direction" (may not be necessary anyway), allows picking a
|
||
// bad initial switch direction
|
||
// - track tool should add a switch to a track on right-click, if possible (and also probably delete
|
||
// it on ctrl-right-click?)
|
||
// - no preview tile with force floor or track tool
|
||
// - no ice drawing tool
|
||
// - cursor box shows with selection tool which seems inappropriate
|
||
// - controls do not exactly stand out and are just plain text
|
||
const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently
|
||
export class MouseOperation {
|
||
constructor(editor, physical_button) {
|
||
this.editor = editor;
|
||
this.is_held = false;
|
||
this.physical_button = physical_button;
|
||
this.alt_mode = physical_button !== 0;
|
||
this.ctrl = false;
|
||
this.shift = false;
|
||
|
||
// Client coordinates of the previous mouse event
|
||
this.prev_client_x = null;
|
||
this.prev_client_y = null;
|
||
// Cell coordinates
|
||
this.prev_cell_x = null;
|
||
this.prev_cell_y = null;
|
||
// Fractional cell coordinates
|
||
this.prev_frac_cell_x = null;
|
||
this.prev_frac_cell_y = null;
|
||
|
||
// Same as above, but for the most recent click (so drag ops know where they started)
|
||
this.click_client_x = null;
|
||
this.click_client_y = null;
|
||
this.click_cell_x = null;
|
||
this.click_cell_y = null;
|
||
this.click_frac_cell_x = null;
|
||
this.click_frac_cell_y = null;
|
||
}
|
||
|
||
cell(x, y) {
|
||
return this.editor.cell(Math.floor(x), Math.floor(y));
|
||
}
|
||
|
||
do_press(ev) {
|
||
this.is_held = true;
|
||
this._update_modifiers(ev);
|
||
|
||
this.client_x = ev.clientX;
|
||
this.client_y = ev.clientY;
|
||
[this.click_frac_cell_x, this.click_frac_cell_y] = this.editor.renderer.real_cell_coords_from_event(ev);
|
||
this.click_cell_x = Math.floor(this.click_frac_cell_x);
|
||
this.click_cell_y = Math.floor(this.click_frac_cell_y);
|
||
|
||
this.prev_client_x = this.client_x;
|
||
this.prev_client_y = this.client_y;
|
||
this.prev_frac_cell_x = this.click_frac_cell_x;
|
||
this.prev_frac_cell_y = this.click_frac_cell_y;
|
||
this.prev_cell_x = this.click_cell_x;
|
||
this.prev_cell_y = this.click_cell_y;
|
||
|
||
this.handle_press(this.click_cell_x, this.click_cell_y, ev);
|
||
}
|
||
|
||
do_move(ev) {
|
||
this._update_modifiers(ev);
|
||
let [frac_cell_x, frac_cell_y] = this.editor.renderer.real_cell_coords_from_event(ev);
|
||
let cell_x = Math.floor(frac_cell_x);
|
||
let cell_y = Math.floor(frac_cell_y);
|
||
|
||
if (this.is_held && (ev.buttons & MOUSE_BUTTON_MASKS[this.physical_button]) === 0) {
|
||
this.do_abort();
|
||
}
|
||
|
||
if (this.is_held) {
|
||
// Continue a drag even if the mouse goes outside the viewport
|
||
this.handle_drag(ev.clientX, ev.clientY, frac_cell_x, frac_cell_y, cell_x, cell_y);
|
||
}
|
||
else {
|
||
// This is a hover, which has separate behavior for losing track of the mouse. Note
|
||
// that we can't just check if the cell coordinates are valid; we also need to know that
|
||
// the mouse is actually over the visible viewport (the canvas may have scrolled!)
|
||
let in_bounds = false;
|
||
if (this.editor.is_in_bounds(cell_x, cell_y)) {
|
||
let rect = this.editor.actual_viewport_el.getBoundingClientRect();
|
||
let cx = ev.clientX, cy = ev.clientY;
|
||
if (rect.left <= cx && cx < rect.right && rect.top <= cy && cy < rect.bottom) {
|
||
in_bounds = true;
|
||
}
|
||
}
|
||
|
||
if (in_bounds) {
|
||
this.show();
|
||
this.handle_hover(ev.clientX, ev.clientY, frac_cell_x, frac_cell_y, cell_x, cell_y);
|
||
}
|
||
else {
|
||
this.hide();
|
||
this.handle_leave();
|
||
}
|
||
}
|
||
|
||
this.prev_client_x = ev.clientX;
|
||
this.prev_client_y = ev.clientY;
|
||
this.prev_frac_cell_x = frac_cell_x;
|
||
this.prev_frac_cell_y = frac_cell_y;
|
||
this.prev_cell_x = cell_x;
|
||
this.prev_cell_y = cell_y;
|
||
}
|
||
|
||
do_leave() {
|
||
this.hide();
|
||
}
|
||
|
||
show() {
|
||
if (! this.is_hover_visible) {
|
||
this.is_hover_visible = true;
|
||
this.editor.preview_g.style.display = '';
|
||
}
|
||
}
|
||
|
||
hide() {
|
||
if (this.is_hover_visible) {
|
||
this.is_hover_visible = false;
|
||
this.editor.preview_g.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
_update_modifiers(ev) {
|
||
this.ctrl = ev.ctrlKey;
|
||
this.shift = ev.shiftKey;
|
||
this.alt = ev.altKey;
|
||
}
|
||
|
||
clear_modifiers() {
|
||
this.ctrl = false;
|
||
this.shift = false;
|
||
this.alt = false;
|
||
}
|
||
|
||
do_commit() {
|
||
if (! this.is_held)
|
||
return;
|
||
|
||
this.commit_press();
|
||
this.cleanup_press();
|
||
this.is_held = false;
|
||
}
|
||
|
||
do_abort() {
|
||
if (! this.is_held)
|
||
return;
|
||
|
||
this.abort_press();
|
||
this.cleanup_press();
|
||
this.is_held = false;
|
||
}
|
||
|
||
do_destroy() {
|
||
this.do_abort();
|
||
this.cleanup_hover();
|
||
}
|
||
|
||
*iter_touched_cells(frac_cell_x, frac_cell_y) {
|
||
for (let pt of walk_grid(
|
||
this.prev_frac_cell_x, this.prev_frac_cell_y, frac_cell_x, frac_cell_y,
|
||
// Bound the grid walk to one cell beyond the edges of the level, so that dragging the
|
||
// mouse in from outside the actual edges still works reliably
|
||
-1, -1, this.editor.stored_level.size_x, this.editor.stored_level.size_y))
|
||
{
|
||
if (this.editor.is_in_bounds(...pt)) {
|
||
yield pt;
|
||
}
|
||
}
|
||
}
|
||
|
||
// -- Implement these --
|
||
|
||
// Called when the mouse button is first pressed
|
||
handle_press(x, y, ev) {}
|
||
// Called when the mouse is moved while the button is held down
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {}
|
||
// Called when releasing the mouse button
|
||
commit_press() {}
|
||
// Called when aborting a held mouse, e.g. by pressing Esc or losing focus
|
||
abort_press() {}
|
||
// Called after either of the above cases
|
||
cleanup_press() {}
|
||
|
||
// Called when the mouse is moved while the button is NOT held down
|
||
handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {}
|
||
// Called when the foreground or background tile changes (after it's been redrawn)
|
||
handle_tile_updated(is_bg = false) {}
|
||
// Called when the mouse leaves the level or viewport while the button is NOT held down
|
||
handle_leave() {}
|
||
// Called when the hover ends??
|
||
cleanup_hover() {}
|
||
}
|
||
|
||
export class PanOperation extends MouseOperation {
|
||
handle_drag(client_x, client_y) {
|
||
let target = this.editor.actual_viewport_el;
|
||
let dx = this.prev_client_x - client_x;
|
||
let dy = this.prev_client_y - client_y;
|
||
target.scrollLeft += dx;
|
||
target.scrollTop += dy;
|
||
}
|
||
}
|
||
|
||
// FIXME handle moving the mouse while the button is down; should continuously eyedrop
|
||
// (seems like that /should/ work...)
|
||
export class EyedropOperation extends MouseOperation {
|
||
constructor(...args) {
|
||
super(...args);
|
||
// Last coordinates we clicked on
|
||
// FIXME whoops, storing this state locally doesn't work since we're destroyed between
|
||
// clicks lol! clean fix is to make an op immediately and persist it even when mouse isn't
|
||
// down? then the hover stuff could be rolled into the tool too? kind of a big change tho
|
||
// so for now let's cheat and hack it onto the editor itself
|
||
this.last_eyedropped_coords = null;
|
||
this.last_layer = null;
|
||
}
|
||
|
||
eyedrop(x, y) {
|
||
let cell = this.cell(x, y);
|
||
if (! cell) {
|
||
this.last_eyedropped_coords = null;
|
||
return;
|
||
}
|
||
|
||
// If we're picking the background, we always use the terrain
|
||
if (this.ctrl) {
|
||
this.editor.select_background_tile(cell[LAYERS.terrain]);
|
||
return;
|
||
}
|
||
|
||
// Pick the topmost thing, unless we're clicking on a cell repeatedly, in which case we
|
||
// continue from below the last thing we picked
|
||
let layer_offset = 0;
|
||
if (this.last_eyedropped_coords &&
|
||
this.last_eyedropped_coords[0] === x && this.last_eyedropped_coords[1] === y)
|
||
{
|
||
layer_offset = this.last_layer;
|
||
}
|
||
for (let l = LAYERS.MAX - 1; l >= 0; l--) {
|
||
// This scheme means we'll cycle back around after hitting the bottom
|
||
let layer = (l + layer_offset) % LAYERS.MAX;
|
||
let tile = cell[layer];
|
||
if (! tile)
|
||
continue;
|
||
|
||
this.editor.select_foreground_tile(tile);
|
||
this.last_eyedropped_coords = [x, y];
|
||
this.last_layer = layer;
|
||
return;
|
||
}
|
||
}
|
||
|
||
handle_press(x, y) {
|
||
this.eyedrop(x, y);
|
||
}
|
||
handle_drag(x, y) {
|
||
// FIXME should only re-eyedrop if we enter a new cell or click again
|
||
this.eyedrop(x, y);
|
||
}
|
||
}
|
||
|
||
|
||
export class PencilOperation extends MouseOperation {
|
||
constructor(...args) {
|
||
super(...args);
|
||
|
||
this.image = mk_svg('image', {
|
||
id: 'svg-editor-preview-tile',
|
||
x: 0,
|
||
y: 0,
|
||
width: 1,
|
||
height: 1,
|
||
});
|
||
this.editor.preview_g.append(this.image);
|
||
this.handle_tile_updated();
|
||
}
|
||
|
||
// Hover: draw the tile in the pointed-to cell
|
||
handle_tile_updated(is_bg = false) {
|
||
if (is_bg)
|
||
return;
|
||
this.image.setAttribute('href', this.editor.fg_tile_el.toDataURL());
|
||
}
|
||
handle_hover(_mx, _my, _cxf, _cyf, cell_x, cell_y) {
|
||
this.image.setAttribute('x', cell_x);
|
||
this.image.setAttribute('y', cell_y);
|
||
}
|
||
cleanup_hover() {
|
||
this.image.remove();
|
||
}
|
||
|
||
handle_press(x, y) {
|
||
this.draw_in_cell(x, y);
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||
for (let [x, y] of this.iter_touched_cells(frac_cell_x, frac_cell_y)) {
|
||
this.draw_in_cell(x, y);
|
||
}
|
||
|
||
// Also update the preview tile position
|
||
this.handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y);
|
||
}
|
||
|
||
draw_in_cell(x, y) {
|
||
let template = this.editor.fg_tile;
|
||
let cell = this.cell(x, y);
|
||
if (this.ctrl) {
|
||
// Erase
|
||
if (this.shift) {
|
||
// Wipe the whole cell
|
||
let new_cell = this.editor.make_blank_cell(x, y);
|
||
this.editor.replace_cell(cell, new_cell);
|
||
}
|
||
else if (template) {
|
||
// Erase whatever's on the same layer as the fg tile
|
||
this.editor.erase_tile(cell);
|
||
}
|
||
}
|
||
else {
|
||
// Draw
|
||
if (! template)
|
||
return;
|
||
if (this.shift) {
|
||
// Aggressive mode: replace whatever's already in the cell
|
||
let new_cell = this.editor.make_blank_cell(x, y);
|
||
new_cell[template.type.layer] = {...template};
|
||
this.editor.replace_cell(cell, new_cell);
|
||
}
|
||
else {
|
||
// Default operation: only replace whatever's on the same layer
|
||
this.editor.place_in_cell(cell, template);
|
||
}
|
||
}
|
||
}
|
||
|
||
cleanup_press() {
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
|
||
// FIXME still to do on this:
|
||
// - doesn't know to update canvas size or erase results when a new level is loaded OR when the
|
||
// level size changes (and for that matter the selection tool doesn't either)
|
||
// - hold shift to replace all of the same tile in the whole level? (need to know when shift is
|
||
// toggled)
|
||
// - right-click to pick, same logic as pencil (which needs improving)
|
||
// - ctrl-click to erase
|
||
// - wait, no. ctrl to like, fill the terrain layer regardless of the current tile's layer? atm
|
||
// you can't flood with an item usefully, it just fills the whole level
|
||
// - reset the preview after a fill? is that ever necessary?
|
||
export class FillOperation extends MouseOperation {
|
||
constructor(...args) {
|
||
super(...args);
|
||
let renderer = this.editor.renderer;
|
||
this.canvas = mk('canvas', {
|
||
width: renderer.canvas.width,
|
||
height: renderer.canvas.height,
|
||
});
|
||
this.foreign_object = mk_svg('foreignObject', {
|
||
x: 0, y: 0,
|
||
width: this.canvas.width, height: this.canvas.height,
|
||
transform: `scale(${1/renderer.tileset.size_x} ${1/renderer.tileset.size_y})`,
|
||
}, this.canvas);
|
||
this.editor.preview_g.append(this.foreign_object);
|
||
|
||
// array of (true: in flood, false: definitely not), or null if not yet populated
|
||
this.fill_state = null;
|
||
// Last coordinates we updated from
|
||
// FIXME probably not necessary now?
|
||
this.last_known_coords = null;
|
||
// Palette tile we last flooded with
|
||
this.last_known_tile = this.editor.fg_tile;
|
||
}
|
||
|
||
handle_hover(_mx, _my, _gxf, _gyf, cell_x, cell_y) {
|
||
this.last_known_coords = [cell_x, cell_y];
|
||
this.last_known_tile = this.editor.fg_tile;
|
||
this._floodfill_from(cell_x, cell_y);
|
||
}
|
||
_floodfill_from(x0, y0) {
|
||
let i0 = this.editor.stored_level.coords_to_scalar(x0, y0);
|
||
if (this.fill_state && this.fill_state[i0]) {
|
||
// This cell is already part of the pending fill, so there's nothing to do
|
||
return;
|
||
}
|
||
|
||
let stored_level = this.editor.stored_level;
|
||
let tile = this.editor.fg_tile;
|
||
let layer = tile.type.layer;
|
||
let tile0 = stored_level.linear_cells[i0][layer] ?? null;
|
||
let type0 = tile0 ? tile0.type : null;
|
||
|
||
if (! this.editor.selection.contains(x0, y0)) {
|
||
this.fill_state = null;
|
||
this._redraw();
|
||
return;
|
||
}
|
||
|
||
// Aaand, floodfill
|
||
this.fill_state = new Array(stored_level.linear_cells.length);
|
||
this.fill_state[i0] = true;
|
||
let pending = [i0];
|
||
let steps = 0;
|
||
while (pending.length > 0) {
|
||
let old_pending = pending;
|
||
pending = [];
|
||
for (let i of old_pending) {
|
||
let [x, y] = stored_level.scalar_to_coords(i);
|
||
|
||
// Check neighbors
|
||
for (let dirinfo of Object.values(DIRECTIONS)) {
|
||
let [dx, dy] = dirinfo.movement;
|
||
let nx = x + dx;
|
||
let ny = y + dy;
|
||
let j = stored_level.coords_to_scalar(nx, ny)
|
||
if (! this.editor.selection.contains(nx, ny)) {
|
||
this.fill_state[j] = false;
|
||
continue;
|
||
}
|
||
|
||
let cell = this.editor.cell(nx, ny);
|
||
if (cell) {
|
||
if (this.fill_state[j] !== undefined)
|
||
continue;
|
||
|
||
let tile = cell[layer] ?? null;
|
||
let type = tile ? tile.type : null;
|
||
if (type === type0) {
|
||
this.fill_state[j] = true;
|
||
pending.push(j);
|
||
}
|
||
else {
|
||
this.fill_state[j] = false;
|
||
}
|
||
}
|
||
}
|
||
steps += 1;
|
||
if (steps > 10000) {
|
||
console.error("more steps than should be possible");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
this._redraw();
|
||
}
|
||
|
||
_redraw() {
|
||
// Draw all the good tiles
|
||
let ctx = this.canvas.getContext('2d');
|
||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
if (! this.fill_state)
|
||
return;
|
||
|
||
let stored_level = this.editor.stored_level;
|
||
let tileset = this.editor.renderer.tileset;
|
||
let source = this.editor.fg_tile_el;
|
||
for (let [i, ok] of this.fill_state.entries()) {
|
||
if (! ok)
|
||
continue;
|
||
|
||
let [x, y] = stored_level.scalar_to_coords(i);
|
||
ctx.drawImage(source, x * tileset.size_x, y * tileset.size_y);
|
||
}
|
||
}
|
||
|
||
handle_tile_updated(is_bg = false) {
|
||
if (is_bg)
|
||
// TODO
|
||
return;
|
||
|
||
// Figure out whether the floodfill results changed. If the new tile is on the same layer
|
||
// as the old tile, we can reuse the results and just redraw. If not, recompute everything
|
||
// (unless we're hidden, in which case blow it away and just do nothing).
|
||
if (this.editor.fg_tile.type.layer === this.last_known_tile.type.layer) {
|
||
if (this.fill_state) {
|
||
this._redraw();
|
||
}
|
||
}
|
||
else {
|
||
this.fill_state = null;
|
||
if (this.last_known_coords && ! this.hidden) {
|
||
this._floodfill_from(...this.last_known_coords);
|
||
}
|
||
}
|
||
}
|
||
|
||
cleanup_hover() {
|
||
this.foreign_object.remove();
|
||
}
|
||
|
||
handle_press() {
|
||
// Filling is a single-click thing, and all the work was done while hovering
|
||
if (! this.fill_state) {
|
||
// Something has gone terribly awry (or they clicked outside the level)
|
||
return;
|
||
}
|
||
|
||
let stored_level = this.editor.stored_level;
|
||
let template = this.editor.fg_tile;
|
||
for (let [i, ok] of this.fill_state.entries()) {
|
||
if (! ok)
|
||
continue;
|
||
|
||
let cell = this.editor.cell(...stored_level.scalar_to_coords(i));
|
||
this.editor.place_in_cell(cell, template);
|
||
}
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
|
||
|
||
// TODO also, delete
|
||
// FIXME i broke transforms
|
||
// FIXME need to subtract from selection too
|
||
export class SelectOperation extends MouseOperation {
|
||
handle_press() {
|
||
if (this.shift) {
|
||
// Extend selection
|
||
this.mode = 'extend';
|
||
this.pending_selection = this.editor.selection.create_pending();
|
||
this.update_pending_selection();
|
||
}
|
||
else if (! this.editor.selection.is_empty &&
|
||
this.editor.selection.contains(this.click_cell_x, this.click_cell_y))
|
||
{
|
||
// Move existing selection
|
||
this.mode = 'float';
|
||
this.make_copy = this.ctrl;
|
||
}
|
||
else {
|
||
// Create new selection
|
||
this.mode = 'create';
|
||
this.pending_selection = this.editor.selection.create_pending();
|
||
this.update_pending_selection();
|
||
}
|
||
this.has_moved = false;
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||
if (this.mode === 'float') {
|
||
if (this.has_moved) {
|
||
this.editor.selection.move_by(Math.floor(cell_x - this.prev_cell_x), Math.floor(cell_y - this.prev_cell_y));
|
||
return;
|
||
}
|
||
|
||
if (this.make_copy) {
|
||
if (this.editor.selection.is_floating) {
|
||
// Stamp the floating selection but keep it floating
|
||
this.editor.selection.stamp_float(true);
|
||
}
|
||
else {
|
||
this.editor.selection.enfloat(true);
|
||
}
|
||
}
|
||
else if (! this.editor.selection.is_floating) {
|
||
this.editor.selection.enfloat();
|
||
}
|
||
}
|
||
else {
|
||
this.update_pending_selection();
|
||
}
|
||
this.has_moved = true;
|
||
}
|
||
|
||
update_pending_selection() {
|
||
this.pending_selection.set_extrema(this.click_cell_x, this.click_cell_y, this.prev_cell_x, this.prev_cell_y);
|
||
}
|
||
|
||
commit_press() {
|
||
if (this.mode === 'float') {
|
||
// Make selection move undoable
|
||
let dx = Math.floor(this.prev_cell_x - this.click_cell_x);
|
||
let dy = Math.floor(this.prev_cell_y - this.click_cell_y);
|
||
if (dx || dy) {
|
||
this.editor._done(
|
||
() => this.editor.selection.move_by(dx, dy),
|
||
() => this.editor.selection.move_by(-dx, -dy),
|
||
);
|
||
}
|
||
}
|
||
else { // create/extend
|
||
if (this.has_moved) {
|
||
// Drag either creates or extends the selection
|
||
// If there's an existing floating selection (which isn't what we're operating on),
|
||
// commit it before doing anything else
|
||
this.editor.selection.commit_floating();
|
||
|
||
if (this.mode === 'create') {
|
||
this.editor.selection.clear();
|
||
}
|
||
this.pending_selection.commit();
|
||
}
|
||
else {
|
||
// Plain click clears selection. But first, if there's a floating selection and
|
||
// it's moved, commit that movement as a separate undo entry
|
||
if (this.editor.selection.is_floating) {
|
||
let float_moved = this.editor.selection.has_moved;
|
||
if (float_moved) {
|
||
this.editor.commit_undo();
|
||
}
|
||
this.editor.selection.commit_floating();
|
||
}
|
||
|
||
this.pending_selection.discard();
|
||
this.editor.selection.clear();
|
||
}
|
||
}
|
||
this.editor.commit_undo();
|
||
}
|
||
abort_press() {
|
||
if (this.mode === 'float') {
|
||
// FIXME revert the move?
|
||
}
|
||
else {
|
||
this.pending_selection.discard();
|
||
}
|
||
}
|
||
|
||
do_destroy() {
|
||
// Don't let a floating selection persist when switching tools
|
||
this.editor.selection.commit_floating();
|
||
this.editor.commit_undo();
|
||
super.do_destroy();
|
||
}
|
||
}
|
||
|
||
export class ForceFloorOperation extends MouseOperation {
|
||
handle_press(x, y) {
|
||
// Begin by placing an all-way force floor under the mouse
|
||
this.editor.place_in_cell(this.cell(x, y), {type: TILE_TYPES.force_floor_all});
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y) {
|
||
// Walk the mouse movement and change each we touch to match the direction we
|
||
// crossed the border
|
||
// FIXME occasionally i draw a tetris S kinda shape and both middle parts point
|
||
// the same direction, but shouldn't
|
||
let i = 0;
|
||
let prevx, prevy;
|
||
for (let [x, y] of this.iter_touched_cells(frac_cell_x, frac_cell_y)) {
|
||
i += 1;
|
||
// The very first cell is the one the mouse was already in, and we don't
|
||
// have a movement direction yet, so leave that alone
|
||
if (i === 1) {
|
||
prevx = x;
|
||
prevy = y;
|
||
continue;
|
||
}
|
||
let name;
|
||
if (x === prevx) {
|
||
if (y > prevy) {
|
||
name = 'force_floor_s';
|
||
}
|
||
else {
|
||
name = 'force_floor_n';
|
||
}
|
||
}
|
||
else {
|
||
if (x > prevx) {
|
||
name = 'force_floor_e';
|
||
}
|
||
else {
|
||
name = 'force_floor_w';
|
||
}
|
||
}
|
||
|
||
// The second cell tells us the direction to use for the first, assuming it
|
||
// had some kind of force floor
|
||
if (i === 2) {
|
||
let prevcell = this.editor.cell(prevx, prevy);
|
||
if (prevcell[LAYERS.terrain].type.name.startsWith('force_floor_')) {
|
||
this.editor.place_in_cell(prevcell, {type: TILE_TYPES[name]});
|
||
}
|
||
}
|
||
|
||
// Drawing a loop with force floors creates ice (but not in the previous
|
||
// cell, obviously)
|
||
let cell = this.editor.cell(x, y);
|
||
if (cell[LAYERS.terrain].type.name.startsWith('force_floor_') &&
|
||
cell[LAYERS.terrain].type.name !== name)
|
||
{
|
||
name = 'ice';
|
||
}
|
||
this.editor.place_in_cell(cell, {type: TILE_TYPES[name]});
|
||
|
||
prevx = x;
|
||
prevy = y;
|
||
}
|
||
}
|
||
cleanup_press() {
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
|
||
// TODO entered cell should get blank railroad?
|
||
// TODO maybe place a straight track in the new cell so it looks like we're doing something, then
|
||
// fix it if it wasn't there?
|
||
// TODO gonna need an ice tool too, so maybe i can merge all three with some base thing that tracks
|
||
// the directions the mouse is moving? or is FF tool too different?
|
||
export class TrackOperation extends MouseOperation {
|
||
handle_press() {
|
||
// Do nothing to start; we only lay track when the mouse leaves a cell
|
||
this.entry_direction = null;
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y) {
|
||
// Walk the mouse movement and, for every tile we LEAVE, add a railroad track matching the
|
||
// two edges of it that we crossed.
|
||
let prevx = null, prevy = null;
|
||
for (let [x, y] of this.iter_touched_cells(frac_cell_x, frac_cell_y)) {
|
||
if (prevx === null || prevy === null) {
|
||
prevx = x;
|
||
prevy = y;
|
||
continue;
|
||
}
|
||
|
||
// Figure out which way we're leaving the tile
|
||
let exit_direction;
|
||
if (x === prevx) {
|
||
if (y > prevy) {
|
||
exit_direction = 'south';
|
||
}
|
||
else {
|
||
exit_direction = 'north';
|
||
}
|
||
}
|
||
else {
|
||
if (x > prevx) {
|
||
exit_direction = 'east';
|
||
}
|
||
else {
|
||
exit_direction = 'west';
|
||
}
|
||
}
|
||
|
||
// If the entry direction is missing or bogus, lay straight track
|
||
if (this.entry_direction === null || this.entry_direction === exit_direction) {
|
||
this.entry_direction = DIRECTIONS[exit_direction].opposite;
|
||
}
|
||
|
||
// Get the corresponding bit
|
||
let bit = null;
|
||
for (let [i, track] of TILE_TYPES['railroad'].track_order.entries()) {
|
||
if ((track[0] === this.entry_direction && track[1] === exit_direction) ||
|
||
(track[1] === this.entry_direction && track[0] === exit_direction))
|
||
{
|
||
bit = 1 << i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (bit === null)
|
||
continue;
|
||
|
||
// Update the cell we just left
|
||
let cell = this.cell(prevx, prevy);
|
||
let terrain = cell[0];
|
||
if (terrain.type.name === 'railroad') {
|
||
let new_terrain = {...terrain};
|
||
if (this.ctrl) {
|
||
// Erase
|
||
// TODO fix track switch?
|
||
// TODO if this leaves tracks === 0, replace with floor?
|
||
new_terrain.tracks &= ~bit;
|
||
}
|
||
else {
|
||
// Draw
|
||
new_terrain.tracks |= bit;
|
||
}
|
||
this.editor.place_in_cell(cell, new_terrain);
|
||
}
|
||
else if (! this.ctrl) {
|
||
terrain = { type: TILE_TYPES['railroad'] };
|
||
terrain.type.populate_defaults(terrain);
|
||
terrain.tracks |= bit;
|
||
this.editor.place_in_cell(cell, terrain);
|
||
}
|
||
|
||
prevx = x;
|
||
prevy = y;
|
||
this.entry_direction = DIRECTIONS[exit_direction].opposite;
|
||
}
|
||
}
|
||
cleanup_press() {
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
|
||
export class ConnectOperation extends MouseOperation {
|
||
handle_press(x, y) {
|
||
// TODO restrict to button/cloner unless holding shift
|
||
// TODO what do i do when you erase a button/cloner? can i detect if you're picking it up?
|
||
let src = this.editor.stored_level.coords_to_scalar(x, y);
|
||
if (this.alt_mode) {
|
||
// Auto connect using Lynx rules
|
||
let cell = this.cell(x, y);
|
||
let terrain = cell[LAYERS.terrain];
|
||
let other = null;
|
||
let swap = false;
|
||
if (terrain.type.name === 'button_red') {
|
||
other = this.search_for(src, 'cloner', 1);
|
||
}
|
||
else if (terrain.type.name === 'cloner') {
|
||
other = this.search_for(src, 'button_red', -1);
|
||
swap = true;
|
||
}
|
||
else if (terrain.type.name === 'button_brown') {
|
||
other = this.search_for(src, 'trap', 1);
|
||
}
|
||
else if (terrain.type.name === 'trap') {
|
||
other = this.search_for(src, 'button_brown', -1);
|
||
swap = true;
|
||
}
|
||
|
||
if (other !== null) {
|
||
if (swap) {
|
||
this.editor.set_custom_connection(other, src);
|
||
}
|
||
else {
|
||
this.editor.set_custom_connection(src, other);
|
||
}
|
||
this.editor.commit_undo();
|
||
}
|
||
return;
|
||
}
|
||
this.pending_cxn = new SVGConnection(x, y, x, y);
|
||
this.editor.svg_overlay.append(this.pending_cxn.element);
|
||
}
|
||
// FIXME this is hella the sort of thing that should be on Editor, or in algorithms
|
||
search_for(i0, name, dir) {
|
||
let l = this.editor.stored_level.linear_cells.length;
|
||
let i = i0;
|
||
while (true) {
|
||
i += dir;
|
||
if (i < 0) {
|
||
i += l;
|
||
}
|
||
else if (i >= l) {
|
||
i -= l;
|
||
}
|
||
if (i === i0)
|
||
return null;
|
||
|
||
let cell = this.editor.stored_level.linear_cells[i];
|
||
let tile = cell[LAYERS.terrain];
|
||
if (tile.type.name === name) {
|
||
return i;
|
||
}
|
||
}
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||
}
|
||
commit_press() {
|
||
}
|
||
abort_press() {
|
||
this.pending_cxn.element.remove();
|
||
}
|
||
cleanup_press() {
|
||
}
|
||
}
|
||
export class WireOperation extends MouseOperation {
|
||
handle_press() {
|
||
if (this.alt_mode) {
|
||
// Place or remove wire tunnels
|
||
// TODO this could just be a separate tool now
|
||
let cell = this.cell(this.click_frac_cell_x, this.click_frac_cell_y);
|
||
if (! cell)
|
||
return;
|
||
|
||
let direction;
|
||
// Use the offset from the center to figure out which edge of the tile to affect
|
||
let xoff = this.click_frac_cell_x % 1 - 0.5;
|
||
let yoff = this.click_frac_cell_y % 1 - 0.5;
|
||
if (Math.abs(xoff) > Math.abs(yoff)) {
|
||
if (xoff > 0) {
|
||
direction = 'east';
|
||
}
|
||
else {
|
||
direction = 'west';
|
||
}
|
||
}
|
||
else {
|
||
if (yoff > 0) {
|
||
direction = 'south';
|
||
}
|
||
else {
|
||
direction = 'north';
|
||
}
|
||
}
|
||
let bit = DIRECTIONS[direction].bit;
|
||
|
||
let terrain = cell[LAYERS.terrain];
|
||
if (terrain.type.name === 'floor') {
|
||
terrain = {...terrain};
|
||
// TODO if this ever supports drag, remember whether we're adding or removing
|
||
// initially
|
||
if (terrain.wire_tunnel_directions & bit) {
|
||
terrain.wire_tunnel_directions &= ~bit;
|
||
}
|
||
else {
|
||
terrain.wire_tunnel_directions |= bit;
|
||
}
|
||
this.editor.place_in_cell(cell, terrain);
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y) {
|
||
if (this.alt_mode) {
|
||
// Wire tunnels don't support dragging
|
||
// TODO but maybe they should?? makes erasing a lot of them easier at least
|
||
return;
|
||
}
|
||
|
||
// Wire is interesting. Consider this diagram.
|
||
// +-------+
|
||
// | . A . |
|
||
// |...A...|
|
||
// | . A . |
|
||
// |BBB+CCC|
|
||
// | . D . |
|
||
// |...D...|
|
||
// | . D . |
|
||
// +-------+
|
||
// In order to know which of the four wire pieces in a cell (A, B, C, D) someone is trying
|
||
// to draw over, we use a quarter-size grid, indicated by the dots. Then any mouse movement
|
||
// that crosses the first horizontal grid line means we should draw wire A.
|
||
// (Note that crossing either a tile boundary or the middle of a cell doesn't mean anything;
|
||
// for example, dragging the mouse horizontally across the A wire is meaningless.)
|
||
// TODO maybe i should just have a walk_grid variant that yields line crossings, christ
|
||
let prevqx = null, prevqy = null;
|
||
for (let [qx, qy] of walk_grid(
|
||
this.prev_frac_cell_x * 4, this.prev_frac_cell_y * 4, frac_cell_x * 4, frac_cell_y * 4,
|
||
// See comment in iter_touched_cells
|
||
-1, -1, this.editor.stored_level.size_x * 4, this.editor.stored_level.size_y * 4))
|
||
{
|
||
if (prevqx === null || prevqy === null) {
|
||
prevqx = qx;
|
||
prevqy = qy;
|
||
continue;
|
||
}
|
||
|
||
// Figure out which grid line we've crossed; direction doesn't matter, so we just get
|
||
// the index of the line, which matches the coordinate of the cell to the right/bottom
|
||
// FIXME 'continue' means we skip the update of prevs, solution is really annoying
|
||
// FIXME if you trace around just the outside of a tile, you'll get absolute nonsense:
|
||
// +---+---+
|
||
// | | |
|
||
// | |.+ |
|
||
// | |.| |
|
||
// +---+.--+
|
||
// | .... |
|
||
// | +-| |
|
||
// | | |
|
||
// +---+---+
|
||
let wire_direction;
|
||
let x, y;
|
||
if (qx === prevqx) {
|
||
// Vertical
|
||
let line = Math.max(qy, prevqy);
|
||
// Even crossings don't correspond to a wire
|
||
if (line % 2 === 0) {
|
||
prevqx = qx;
|
||
prevqy = qy;
|
||
continue;
|
||
}
|
||
|
||
// Convert to real coordinates
|
||
x = Math.floor(qx / 4);
|
||
y = Math.floor(line / 4);
|
||
|
||
if (line % 4 === 1) {
|
||
// Consult the diagram!
|
||
wire_direction = 'north';
|
||
}
|
||
else {
|
||
wire_direction = 'south';
|
||
}
|
||
}
|
||
else {
|
||
// Horizontal; same as above
|
||
let line = Math.max(qx, prevqx);
|
||
if (line % 2 === 0) {
|
||
prevqx = qx;
|
||
prevqy = qy;
|
||
continue;
|
||
}
|
||
|
||
x = Math.floor(line / 4);
|
||
y = Math.floor(qy / 4);
|
||
|
||
if (line % 4 === 1) {
|
||
wire_direction = 'west';
|
||
}
|
||
else {
|
||
wire_direction = 'east';
|
||
}
|
||
}
|
||
|
||
if (! this.editor.is_in_bounds(x, y)) {
|
||
prevqx = qx;
|
||
prevqy = qy;
|
||
continue;
|
||
}
|
||
|
||
let cell = this.cell(x, y);
|
||
for (let tile of Array.from(cell).reverse()) {
|
||
// TODO probably a better way to do this
|
||
if (! tile)
|
||
continue;
|
||
if (! tile.type.contains_wire)
|
||
continue;
|
||
|
||
tile = {...tile};
|
||
tile.wire_directions = tile.wire_directions ?? 0;
|
||
if (this.ctrl) {
|
||
// Erase
|
||
tile.wire_directions &= ~DIRECTIONS[wire_direction].bit;
|
||
}
|
||
else {
|
||
// Draw
|
||
tile.wire_directions |= DIRECTIONS[wire_direction].bit;
|
||
}
|
||
this.editor.place_in_cell(cell, tile);
|
||
break;
|
||
}
|
||
|
||
prevqx = qx;
|
||
prevqy = qy;
|
||
}
|
||
}
|
||
cleanup_press() {
|
||
this.editor.commit_undo();
|
||
}
|
||
}
|
||
|
||
// Tiles the "adjust" tool will turn into each other
|
||
const ADJUST_TOGGLES_CW = {};
|
||
const ADJUST_TOGGLES_CCW = {};
|
||
{
|
||
for (let cycle of [
|
||
['chip', 'chip_extra'],
|
||
// TODO shouldn't this convert regular walls into regular floors then?
|
||
['floor_custom_green', 'wall_custom_green'],
|
||
['floor_custom_pink', 'wall_custom_pink'],
|
||
['floor_custom_yellow', 'wall_custom_yellow'],
|
||
['floor_custom_blue', 'wall_custom_blue'],
|
||
['fake_floor', 'fake_wall'],
|
||
['popdown_floor', 'popdown_wall'],
|
||
['wall_invisible', 'wall_appearing'],
|
||
['green_floor', 'green_wall'],
|
||
['green_bomb', 'green_chip'],
|
||
['purple_floor', 'purple_wall'],
|
||
['thief_keys', 'thief_tools'],
|
||
['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'],
|
||
['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'],
|
||
['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'],
|
||
['ice', 'force_floor_all'],
|
||
['water', 'turtle'],
|
||
['no_player1_sign', 'no_player2_sign'],
|
||
['flame_jet_off', 'flame_jet_on'],
|
||
['light_switch_off', 'light_switch_on'],
|
||
['stopwatch_bonus', 'stopwatch_penalty'],
|
||
['turntable_cw', 'turntable_ccw'],
|
||
])
|
||
{
|
||
for (let [i, tile] of cycle.entries()) {
|
||
let other = cycle[(i + 1) % cycle.length];
|
||
ADJUST_TOGGLES_CW[tile] = other;
|
||
ADJUST_TOGGLES_CCW[other] = tile;
|
||
}
|
||
}
|
||
}
|
||
export class AdjustOperation extends MouseOperation {
|
||
handle_press() {
|
||
let cell = this.cell(this.prev_cell_x, this.prev_cell_y);
|
||
if (this.ctrl) {
|
||
for (let tile of cell) {
|
||
if (tile && TILES_WITH_PROPS[tile.type.name] !== undefined) {
|
||
this.editor.open_tile_prop_overlay(
|
||
tile, cell, this.editor.renderer.get_cell_rect(cell.x, cell.y));
|
||
break;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
let start_layer = this.shift ? 0 : LAYERS.MAX - 1;
|
||
for (let layer = start_layer; layer >= 0; layer--) {
|
||
let tile = cell[layer];
|
||
if (! tile)
|
||
continue;
|
||
|
||
let rotated;
|
||
tile = {...tile}; // TODO little inefficient
|
||
if (this.alt_mode) {
|
||
// Reverse, go counterclockwise
|
||
rotated = this.editor.rotate_tile_left(tile);
|
||
}
|
||
else {
|
||
rotated = this.editor.rotate_tile_right(tile);
|
||
}
|
||
if (rotated) {
|
||
this.editor.place_in_cell(cell, tile);
|
||
this.editor.commit_undo();
|
||
break;
|
||
}
|
||
|
||
// Toggle tiles that go in obvious pairs
|
||
let other = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name];
|
||
if (other) {
|
||
tile.type = TILE_TYPES[other];
|
||
this.editor.place_in_cell(cell, tile);
|
||
this.editor.commit_undo();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// Adjust tool doesn't support dragging
|
||
// TODO should it?
|
||
// TODO if it does then it should end as soon as you spawn a popup
|
||
}
|
||
|
||
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
|
||
// FIXME undo
|
||
// TODO view is not especially visible
|
||
export class CameraOperation extends MouseOperation {
|
||
handle_press(x, y, ev) {
|
||
this.offset_x = 0;
|
||
this.offset_y = 0;
|
||
this.resize_x = 0;
|
||
this.resize_y = 0;
|
||
|
||
let cursor;
|
||
|
||
this.target = ev.target.closest('.overlay-camera');
|
||
if (! this.target) {
|
||
// Clicking in empty space creates a new camera region
|
||
this.mode = 'create';
|
||
cursor = 'move';
|
||
this.region = new DOMRect(this.click_cell_x, this.click_cell_y, 1, 1);
|
||
this.target = mk_svg('rect.overlay-camera', {
|
||
x: this.click_cell_x, y: this.prev_cell_y, width: 1, height: 1,
|
||
'data-region-index': this.editor.stored_level.camera_regions.length,
|
||
});
|
||
this.editor.connections_g.append(this.target);
|
||
}
|
||
else {
|
||
this.region = this.editor.stored_level.camera_regions[parseInt(this.target.getAttribute('data-region-index'), 10)];
|
||
|
||
// If we're grabbing an edge, resize it
|
||
let rect = this.target.getBoundingClientRect();
|
||
let grab_left = (this.click_client_x < rect.left + 16);
|
||
let grab_right = (this.click_client_x > rect.right - 16);
|
||
let grab_top = (this.click_client_y < rect.top + 16);
|
||
let grab_bottom = (this.click_client_y > rect.bottom - 16);
|
||
if (grab_left || grab_right || grab_top || grab_bottom) {
|
||
this.mode = 'resize';
|
||
|
||
if (grab_left) {
|
||
this.resize_edge_x = -1;
|
||
}
|
||
else if (grab_right) {
|
||
this.resize_edge_x = 1;
|
||
}
|
||
else {
|
||
this.resize_edge_x = 0;
|
||
}
|
||
|
||
if (grab_top) {
|
||
this.resize_edge_y = -1;
|
||
}
|
||
else if (grab_bottom) {
|
||
this.resize_edge_y = 1;
|
||
}
|
||
else {
|
||
this.resize_edge_y = 0;
|
||
}
|
||
|
||
if ((grab_top && grab_left) || (grab_bottom && grab_right)) {
|
||
cursor = 'nwse-resize';
|
||
}
|
||
else if ((grab_top && grab_right) || (grab_bottom && grab_left)) {
|
||
cursor = 'nesw-resize';
|
||
}
|
||
else if (grab_top || grab_bottom) {
|
||
cursor = 'ns-resize';
|
||
}
|
||
else {
|
||
cursor = 'ew-resize';
|
||
}
|
||
}
|
||
else {
|
||
this.mode = 'move';
|
||
cursor = 'move';
|
||
}
|
||
}
|
||
|
||
this.editor.viewport_el.style.cursor = cursor;
|
||
|
||
// Create a text element to show the size while editing
|
||
this.size_text = mk_svg('text.overlay-edit-tip');
|
||
this._update_size_text();
|
||
this.editor.svg_overlay.append(this.size_text);
|
||
}
|
||
_update_size_text() {
|
||
this.size_text.setAttribute('x', this.region.x + this.offset_x + (this.region.width + this.resize_x) / 2);
|
||
this.size_text.setAttribute('y', this.region.y + this.offset_y + (this.region.height + this.resize_y) / 2);
|
||
this.size_text.textContent = `${this.region.width + this.resize_x} × ${this.region.height + this.resize_y}`;
|
||
}
|
||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||
// FIXME not right if we zoom, should use frac_cell_x
|
||
let dx = Math.floor((client_x - this.click_client_x) / this.editor.renderer.tileset.size_x + 0.5);
|
||
let dy = Math.floor((client_y - this.click_client_y) / this.editor.renderer.tileset.size_y + 0.5);
|
||
|
||
let stored_level = this.editor.stored_level;
|
||
if (this.mode === 'create') {
|
||
// Just make the new region span between the original click and the new position
|
||
this.region.x = Math.min(cell_x, this.click_cell_x);
|
||
this.region.y = Math.min(cell_y, this.click_cell_y);
|
||
this.region.width = Math.max(cell_x, this.click_cell_x) + 1 - this.region.x;
|
||
this.region.height = Math.max(cell_y, this.click_cell_y) + 1 - this.region.y;
|
||
}
|
||
else if (this.mode === 'move') {
|
||
// Keep it within the map!
|
||
this.offset_x = Math.max(- this.region.x, Math.min(stored_level.size_x - this.region.width, dx));
|
||
this.offset_y = Math.max(- this.region.y, Math.min(stored_level.size_y - this.region.height, dy));
|
||
}
|
||
else {
|
||
// Resize, based on the edge we originally grabbed
|
||
if (this.resize_edge_x < 0) {
|
||
// Left
|
||
dx = Math.max(-this.region.x, Math.min(this.region.width - 1, dx));
|
||
this.resize_x = -dx;
|
||
this.offset_x = dx;
|
||
}
|
||
else if (this.resize_edge_x > 0) {
|
||
// Right
|
||
dx = Math.max(-(this.region.width - 1), Math.min(stored_level.size_x - this.region.right, dx));
|
||
this.resize_x = dx;
|
||
this.offset_x = 0;
|
||
}
|
||
|
||
if (this.resize_edge_y < 0) {
|
||
// Top
|
||
dy = Math.max(-this.region.y, Math.min(this.region.height - 1, dy));
|
||
this.resize_y = -dy;
|
||
this.offset_y = dy;
|
||
}
|
||
else if (this.resize_edge_y > 0) {
|
||
// Bottom
|
||
dy = Math.max(-(this.region.height - 1), Math.min(stored_level.size_y - this.region.bottom, dy));
|
||
this.resize_y = dy;
|
||
this.offset_y = 0;
|
||
}
|
||
}
|
||
|
||
this.target.setAttribute('x', this.region.x + this.offset_x);
|
||
this.target.setAttribute('y', this.region.y + this.offset_y);
|
||
this.target.setAttribute('width', this.region.width + this.resize_x);
|
||
this.target.setAttribute('height', this.region.height + this.resize_y);
|
||
this._update_size_text();
|
||
}
|
||
commit_press() {
|
||
if (this.mode === 'create') {
|
||
// Region is already updated, just add it to the level
|
||
this.editor.stored_level.camera_regions.push(this.region);
|
||
}
|
||
else {
|
||
// Actually edit the underlying region
|
||
this.region.x += this.offset_x;
|
||
this.region.y += this.offset_y;
|
||
this.region.width += this.resize_x;
|
||
this.region.height += this.resize_y;
|
||
}
|
||
}
|
||
abort_press() {
|
||
if (this.mode === 'create') {
|
||
// The element was fake, so delete it
|
||
this.target.remove();
|
||
}
|
||
else {
|
||
// Move the element back to its original location
|
||
this.target.setAttribute('x', this.region.x);
|
||
this.target.setAttribute('y', this.region.y);
|
||
this.target.setAttribute('width', this.region.width);
|
||
this.target.setAttribute('height', this.region.height);
|
||
}
|
||
}
|
||
cleanup_press() {
|
||
this.editor.viewport_el.style.cursor = '';
|
||
this.size_text.remove();
|
||
}
|
||
}
|
||
|
||
export class CameraEraseOperation extends MouseOperation {
|
||
handle_press(x, y, ev) {
|
||
let target = ev.target.closest('.overlay-camera');
|
||
if (target) {
|
||
let index = parseInt(target.getAttribute('data-region-index'), 10);
|
||
target.remove();
|
||
this.editor.stored_level.camera_regions.splice(index, 1);
|
||
}
|
||
}
|
||
}
|