From 2439048f593372e6f9fb94ee16a9d9e1ac75ad82 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Wed, 17 Apr 2024 02:24:06 -0600 Subject: [PATCH] Fix transforming selection + add more transforms --- js/editor/helpers.js | 163 ++++++++++++++++++++++++++++--------------- js/editor/main.js | 158 +++++++++++++++++++++++++++-------------- 2 files changed, 210 insertions(+), 111 deletions(-) diff --git a/js/editor/helpers.js b/js/editor/helpers.js index 7cdd8b1..f87cd53 100644 --- a/js/editor/helpers.js +++ b/js/editor/helpers.js @@ -157,11 +157,12 @@ export class Selection { } else { // Just recreate it from scratch to avoid mixing old and new properties + let new_x = Math.min(this.bbox.x, rect.x); + let new_y = Math.min(this.bbox.y, rect.y); this.bbox = new DOMRect( - Math.min(this.bbox.x, rect.x), - Math.min(this.bbox.y, rect.y), - Math.max(this.bbox.right, rect.right) - this.bbox.x, - Math.max(this.bbox.bottom, rect.bottom) - this.bbox.y); + new_x, new_y, + Math.max(this.bbox.right, rect.right) - new_x, + Math.max(this.bbox.bottom, rect.bottom) - new_y); } this._update_outline(); @@ -398,28 +399,18 @@ export class Selection { this.ring_element.classList.remove('--visible'); } - // TODO only used internally in one place? - *iter_coords() { - let stored_level = this.editor.stored_level; - for (let n of this.cells) { - let [x, y] = stored_level.scalar_to_coords(n); - if (this.floated_offset) { - x += this.floated_offset[0]; - y += this.floated_offset[1]; - } - 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) { - if (this.floated_cells) + if (this.floated_cells) { console.error("Trying to float a selection that's already floating"); + return; + } let floated_cells = new Map; let stored_level = this.editor.stored_level; - for (let [x, y, n] of this.iter_coords()) { + for (let n of this.cells) { + let [x, y] = stored_level.scalar_to_coords(n); let cell = stored_level.linear_cells[n]; if (copy) { floated_cells.set(n, cell.map(tile => tile ? {...tile} : null)); @@ -441,37 +432,32 @@ export class Selection { ); } - // Create floated_canvas and floated_element, based on floated_cells + // Create floated_canvas and floated_element, based on floated_cells, or update them if they + // already exist _init_floated_canvas() { let tileset = this.editor.renderer.tileset; - this.floated_canvas = mk('canvas', { - width: this.bbox.width * tileset.size_x, - height: this.bbox.height * tileset.size_y, - }); - let ctx = this.floated_canvas.getContext('2d'); - for (let n of this.cells) { - let [x, y] = this.editor.stored_level.scalar_to_coords(n); - this.editor.renderer.draw_static_generic({ - // Incredibly stupid hack for just drawing one cell - x0: 0, x1: 0, - y0: 0, y1: 0, - width: 1, - cells: [this.floated_cells.get(n)], - ctx: ctx, - destx: x - this.bbox.left, - desty: y - this.bbox.top, - }); + if (! this.floated_canvas) { + this.floated_canvas = mk('canvas'); } - this.floated_element = mk_svg('g', mk_svg('foreignObject', { - x: 0, - y: 0, - width: this.floated_canvas.width, - height: this.floated_canvas.height, - transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`, - }, this.floated_canvas)); + this.floated_canvas.width = this.bbox.width * tileset.size_x; + this.floated_canvas.height = this.bbox.height * tileset.size_y; + this.redraw(); + + if (! this.floated_element) { + this.floated_element = mk_svg('g', mk_svg('foreignObject', { + x: 0, + y: 0, + transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`, + }, this.floated_canvas)); + // This goes first, so the selection ring still appears on top + this.selection_group.prepend(this.floated_element); + } + let foreign = this.floated_element.querySelector('foreignObject'); + foreign.setAttribute('width', this.floated_canvas.width); + foreign.setAttribute('height', this.floated_canvas.height); + + // The canvas only covers our bbox, so it needs to start where the bbox does this.floated_element.setAttribute('transform', `translate(${this.bbox.x} ${this.bbox.y})`); - // This goes first, so the selection ring still appears on top - this.selection_group.prepend(this.floated_element); } stamp_float(copy = false) { @@ -545,6 +531,65 @@ export class Selection { ); } + // Modifies the cells (and their arrangement) within a floating selection + _rearrange_cells(original_width, convert_coords, upgrade_tile) { + if (! this.floated_cells) + return; + + let new_cells = new Set; + let new_floated_cells = new Map; + let w = this.editor.stored_level.size_x; + let h = this.editor.stored_level.size_y; + for (let n of this.cells) { + // Alas this needs manually computing since the level may have changed size + let x = n % original_width; + let y = Math.floor(n / original_width); + let [x2, y2] = convert_coords(x, y, w, h); + let n2 = x2 + w * y2; + let cell = this.floated_cells.get(n); + cell.x = x2; + cell.y = y2; + for (let tile of cell) { + if (tile) { + upgrade_tile(tile); + } + } + new_cells.add(n2); + new_floated_cells.set(n2, cell); + } + + // Track the old and new centers of the bboxes so the transform can be center-relative + let [cx0, cy0] = convert_coords( + Math.floor(this.bbox.x + this.bbox.width / 2), + Math.floor(this.bbox.y + this.bbox.height / 2), + w, h); + + // Alter the bbox by just transforming two opposite corners + let [x1, y1] = convert_coords(this.bbox.left, this.bbox.top, w, h); + let [x2, y2] = convert_coords(this.bbox.right - 1, this.bbox.bottom - 1, w, h); + let xs = [x1, x2]; + let ys = [y1, y2]; + xs.sort((a, b) => a - b); + ys.sort((a, b) => a - b); + this.bbox = new DOMRect(xs[0], ys[0], xs[1] - xs[0] + 1, ys[1] - ys[0] + 1); + + // Now make it center-relative by shifting the offsets + let [cx1, cy1] = convert_coords( + Math.floor(this.bbox.x + this.bbox.width / 2), + Math.floor(this.bbox.y + this.bbox.height / 2), + w, h); + this.floated_offset[0] += cx1 - cx0; + this.floated_offset[1] += cy1 - cy0; + this._update_floating_transform(); + + // No need for undo; this is undone by performing the reverse operation + this.cells = new_cells; + this.floated_cells = new_floated_cells; + this._init_floated_canvas(); + + this._update_outline(); + } + _delete_floating() { this.selection_group.removeAttribute('transform'); this.ring_element.classList.remove('--floating'); @@ -561,20 +606,22 @@ export class Selection { if (! this.floated_canvas) return; - // FIXME uhoh, how do i actually do this? we have no renderer of our own, we have a - // separate canvas, and all the renderer stuff expects to get ahold of a level. i guess - // refactor it to draw a block of cells? - this.editor.renderer.draw_static_generic({ - x0: 0, y0: 0, - x1: this.rect.width, y1: this.rect.height, - // FIXME this is now a set, not a flat list - cells: this.floated_cells, - width: this.rect.width, - ctx: this.floated_canvas.getContext('2d'), - }); + let ctx = this.floated_canvas.getContext('2d'); + for (let n of this.cells) { + let [x, y] = this.editor.stored_level.scalar_to_coords(n); + this.editor.renderer.draw_static_generic({ + // Incredibly stupid hack for just drawing one cell + x0: 0, x1: 0, + y0: 0, y1: 0, + width: 1, + cells: [this.floated_cells.get(n)], + ctx: ctx, + destx: x - this.bbox.left, + desty: y - this.bbox.top, + }); + } } - // TODO allow floating/dragging, ctrl-dragging to copy, anchoring... // TODO make more stuff respect this (more things should go through Editor for undo reasons anyway) } diff --git a/js/editor/main.js b/js/editor/main.js index 5a47e6a..c3dddfc 100644 --- a/js/editor/main.js +++ b/js/editor/main.js @@ -455,18 +455,27 @@ export class Editor extends PrimaryView { this.redo(); }); let edit_items = [ - ["Rotate CCW", () => { + ["Rotate left", () => { this.rotate_level_left(); }], - ["Rotate CW", () => { + ["Rotate right", () => { this.rotate_level_right(); }], - ["Mirror", () => { + ["Rotate 180°", () => { + this.rotate_level_right(); + }], + ["Mirror horizontally", () => { this.mirror_level(); }], - ["Flip", () => { + ["Flip vertically", () => { this.flip_level(); }], + ["Pivot around main diagonal", () => { + this.pivot_level_main(); + }], + ["Pivot around anti diagonal", () => { + this.pivot_level_anti(); + }], ]; this.edit_menu = new MenuOverlay( this.conductor, @@ -1324,12 +1333,29 @@ export class Editor extends PrimaryView { return this._transform_tile( tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right'); } + rotate_tile_180(tile) { + let changed = this.rotate_tile_right(tile); + changed ||= this.rotate_tile_right(tile); + return changed; + } mirror_tile(tile) { return this._transform_tile(tile, null, 'mirror', 'mirrored'); } flip_tile(tile) { return this._transform_tile(tile, null, 'flip', 'flipped'); } + pivot_tile_main(tile) { + // A flip along the main diagonal is equivalent to a right turn, then a horizontal mirror + let changed = this.rotate_tile_right(tile); + changed ||= this.mirror_tile(tile); + return changed; + } + pivot_tile_anti(tile) { + // A flip along the anti-diagonal is equivalent to a left turn, then a horizontal mirror + let changed = this.rotate_tile_left(tile); + changed ||= this.mirror_tile(tile); + return changed; + } rotate_palette_left() { this.palette_rotation_index += 1; @@ -1561,50 +1587,43 @@ export class Editor extends PrimaryView { // (These don't save undo entries for individual tiles, either, because they're expected to be // completely reversible, and undo is done by performing the opposite transform rather than // reloading a copy of a previous state.) - _rearrange_cells(swap_dimensions, downgrade_coords, upgrade_tile) { - let old_cells, old_w; - let w, h; + _rearrange_cells(swap_dimensions, convert_coords, upgrade_tile) { let new_cells = []; - if (this.selection.is_empty) { - // Do it to the whole level - w = this.stored_level.size_x; - h = this.stored_level.size_y; - old_w = w; - if (swap_dimensions) { - [w, h] = [h, w]; - this.stored_level.size_x = w; - this.stored_level.size_y = h; - } - old_cells = this.stored_level.linear_cells; - this.stored_level.linear_cells = new_cells; - } - else { - // Do it to the selection - w = this.selection.rect.width; - h = this.selection.rect.height; - old_w = w; - if (swap_dimensions) { - [w, h] = [h, w]; - // FIXME this will need more interesting rearranging - this.selection._set_from_rect(new DOMRect( - this.selection.rect.x, this.selection.rect.y, w, h)); - } - old_cells = this.selection.floated_cells; - this.selection.floated_cells = new_cells; + let w = this.stored_level.size_x; + let h = this.stored_level.size_y; + let old_w = w; + let old_h = h; + if (swap_dimensions) { + [w, h] = [h, w]; + this.stored_level.size_x = w; + this.stored_level.size_y = h; } - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - let [old_x, old_y] = downgrade_coords(x, y, w, h); - let cell = old_cells[old_y * old_w + old_x]; + if (! this.selection.is_empty) { + // Do it to the selection + this.selection._rearrange_cells(old_w, convert_coords, upgrade_tile); + return; + } + + let old_cells = this.stored_level.linear_cells; + for (let y = 0; y < old_h; y++) { + for (let x = 0; x < old_w; x++) { + let [x2, y2] = convert_coords(x, y, w, h); + let cell = old_cells[y * old_w + x]; for (let tile of cell) { if (tile) { upgrade_tile(tile); } } - new_cells.push(cell); + let n2 = this.stored_level.coords_to_scalar(x2, y2); + if (new_cells[n2]) { + console.error("Tile transformation overwriting the same cell twice:", x2, y2); + } + new_cells[n2] = cell; } } + + this.stored_level.linear_cells = new_cells; } rotate_level_right() { @@ -1622,6 +1641,11 @@ export class Editor extends PrimaryView { ); } rotate_level_180() { + this._do_transform( + false, + () => this._rotate_level_180(), + () => this._rotate_level_180(), + ); } mirror_level() { this._do_transform( @@ -1637,6 +1661,20 @@ export class Editor extends PrimaryView { () => this._flip_level(), ); } + pivot_level_main() { + this._do_transform( + true, + () => this._pivot_level_main(), + () => this._pivot_level_main(), + ); + } + pivot_level_anti() { + this._do_transform( + true, + () => this._pivot_level_anti(), + () => this._pivot_level_anti(), + ); + } _do_transform(affects_size, redo, undo) { // FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how // those interact with floating selection yet :S @@ -1656,36 +1694,36 @@ export class Editor extends PrimaryView { this.commit_undo(); } _post_transform_cleanup(affects_size) { - if (this.selection.is_empty) { - if (affects_size) { - this.update_after_size_change(); - } - else { - this.redraw_entire_level(); - } - } - else { - // FIXME what if it affects size? - this.selection.redraw(); - } + // The selection takes care of redrawing itself + if (! this.selection.is_empty) + return; + + // We do basically the same work regardless of whether the size changed, so just do it + this.update_after_size_change(); } - // TODO mirror diagonally? // Internal-use versions of the above. These DO NOT create undo entries. _rotate_level_left() { this._rearrange_cells( true, - (x, y, w, h) => [h - 1 - y, x], + (x, y, w, h) => [y, w - 1 - x], tile => this.rotate_tile_left(tile, false), ); } _rotate_level_right() { this._rearrange_cells( true, - (x, y, w, h) => [y, w - 1 - x], + (x, y, w, h) => [h - 1 - y, x], tile => this.rotate_tile_right(tile, false), ); } + _rotate_level_180() { + this._rearrange_cells( + true, + (x, y, w, h) => [w - 1 - x, h - 1 - y], + tile => this.rotate_tile_180(tile, false), + ); + } _mirror_level() { this._rearrange_cells( false, @@ -1700,6 +1738,20 @@ export class Editor extends PrimaryView { tile => this.flip_tile(tile), ); } + _pivot_level_main() { + this._rearrange_cells( + true, + (x, y, w, h) => [y, x], + tile => this.pivot_tile_main(tile), + ); + } + _pivot_level_anti() { + this._rearrange_cells( + true, + (x, y, w, h) => [w - 1 - y, h - 1 - x], + tile => this.pivot_tile_anti(tile), + ); + } // Create a connection between two cells and update the UI accordingly. If dest is null or // undefined, delete any existing connection instead.